<?php

use TEC\Events\Custom_Tables\V1\Models\Occurrence;
use Tribe__Utils__Array as Arr;

if ( ! class_exists( 'Tribe__Tickets__Tickets' ) ) {
	/**
	 * Class with the API definition and common functionality for Tribe Tickets. Providers for this functionality need
	 * to extend this class.
	 *
	 * The relationship between orders, attendees, and event posts is
	 * maintained through post meta fields set for the attendee object.
	 * Implementing classes are expected to provide the following class
	 * constants detailing those meta keys:
	 *
	 *     ATTENDEE_ORDER_KEY
	 *     ATTENDEE_EVENT_KEY
	 *     ATTENDEE_PRODUCT_KEY
	 *
	 * The post type name used for the attendee object should also be
	 * made available via:
	 *
	 *     ATTENDEE_OBJECT
	 *
	 * @since 4.5.0.1 Due to a fatal between Event Ticket Plus extending commerces and this class,
	 *                 we changed this from an Abstract to a normal parent class.
	 */
	class Tribe__Tickets__Tickets {

		/**
		 * Flag used to track if the registration form link has been displayed or not.
		 *
		 * @var boolean
		 */
		private static $have_displayed_reg_link = false;

		/**
		 * Function that is used to store the cache of a specific post associated with a set of tickets, where %d is the
		 * ID of the post being affected.
		 *
		 * @since 4.7.1
		 *
		 * @var string
		 */
		private static $cache_key_prefix = 'tribe_event_tickets_from_';

		/**
		 * All Tribe__Tickets__Tickets api consumers. It's static, so it's shared across all children.
		 *
		 * @var array
		 */
		protected static $active_modules = [];

		/**
		 * Default Tribe__Tickets__Tickets ecommerce module.
		 * It's static, so it's shared across all children.
		 *
		 * @var string
		 */
		protected static $default_module = 'Tribe__Tickets__RSVP';

		/**
		 * Indicates if the frontend ticket form script has already been enqueued (or not).
		 *
		 * @var bool
		 */
		public static $frontend_script_enqueued = false;

		/**
		 * Collection of ticket objects for which we wish to make global stock data available
		 * on the frontend.
		 *
		 * @var array
		 */
		protected static $frontend_ticket_data = [];

		/**
		 * Name of this class. Note that it refers to the child class.
		 *
		 * @var string
		 */
		public $class_name;

		/**
		 * Path of the parent class
		 *
		 * @var string
		 */
		private $parent_path;

		/**
		 * URL of the parent class
		 *
		 * @var string
		 */
		private $parent_url;

		/**
		 * Records batches of tickets that are currently unavailable (used for
		 * displaying the correct "tickets are unavailable" message).
		 *
		 * @var array
		 */
		protected static $currently_unavailable_tickets = [];

		/**
		 * Records posts for which tickets *are* available (used to determine if
		 * a "tickets are unavailable" message should even display).
		 *
		 * @var array
		 */
		protected static $posts_with_available_tickets = [];

		// start API Definitions
		// Child classes must implement all these functions / properties

		/**
		 * Name of the provider
		 *
		 * @var string
		 */
		public $plugin_name;

		/**
		 * Path of the child class
		 *
		 * @var string
		 */
		protected $plugin_path;

		/**
		 * URL of the child class
		 *
		 * @var string
		 */
		protected $plugin_url;

		/**
		 * The name of the post type representing a ticket.
		 *
		 * @var string
		 */
		public $ticket_object = '';

		/**
		 * The name of the meta key used to store whether an attendee is subscribed to updates.
		 *
		 * @since 5.0.3
		 *
		 * @var string
		 */
		public $attendee_subscribed = '_tribe_tickets_subscribed';

		/* Deprecated vars */

		/**
		 * Name of this class. Note that it refers to the child class.
		 * deprecated - use $class_name
		 *
		 * @deprecated 4.6
		 *
		 * @var string
		 */
		public $className;

		/**
		 * Path of the parent class
		 * deprecated - use $parent_path
		 *
		 * @deprecated 4.6
		 *
		 * @var string
		 */
		private $parentPath;

		/**
		 * URL of the parent class
		 * deprecated - use $parent_url
		 *
		 * @deprecated 4.6
		 *
		 * @var string
		 */
		private $parentUrl;

		/**
		 * Name of the provider
		 * deprecated - use $plugin_name
		 *
		 * @deprecated 4.6
		 *
		 * @var string
		 */
		public $pluginName;

		/**
		 * Path of the child class
		 * deprecated - use $plugin_path
		 *
		 * @deprecated 4.6
		 *
		 * @var string
		 */
		protected $pluginPath;

		/**
		 * URL of the child class
		 * deprecated - use $plugin_url
		 *
		 * @deprecated 4.6
		 *
		 * @var string
		 */
		protected $pluginUrl;

		/**
		 * Constant with the Transient Key for Attendees Cache
		 */
		const ATTENDEES_CACHE = 'tribe_attendees';

		/**
		 * Meta key that contains the user id
		 *
		 * @deprecated 4.7 Use the $attendee_user_id variable instead
		 *
		 * @var string
		 */
		const ATTENDEE_USER_ID = '_tribe_tickets_attendee_user_id';

		/**
		 * Meta key that contains the user id
		 *
		 * @var string
		 */
		public $attendee_user_id = '_tribe_tickets_attendee_user_id';

		/**
		 * Name of the CPT that holds Orders
		 */
		public $order_object = '';

		/**
		 * Name of the CPT that holds Attendees.
		 *
		 * @var string
		 */
		public $attendee_object = '';

		/**
		 * Meta key that relates Attendees and Events.
		 *
		 * @var string
		 */
		public $attendee_event_key = '';

		/**
		 * Meta key that relates Attendees and Products.
		 *
		 * @var string
		 */
		public $attendee_product_key = '';

		/**
		 * Indicates if a ticket for this attendee was sent out via email.
		 *
		 * @var boolean
		 */
		public $attendee_ticket_sent = '_tribe_attendee_ticket_sent';

		/**
		 * Logs the attendee notification email activity.
		 *
		 * @var array
		 *
		 * @since 5.1.0
		 */
		public $attendee_activity_log = '_tribe_attendee_activity_log';

		/**
		 * Meta key that if this attendee wants to show on the attendee list
		 *
		 * @var string
		 */
		public $attendee_optout_key = '';

		/**
		 * Meta key that holds the full name of the ticket attendee.
		 *
		 * @since 5.0.3
		 *
		 * @var string
		 */
		public $full_name = '_tribe_tickets_full_name';

		/**
		 * Meta key that holds the email of the ticket attendee.
		 *
		 * @since 5.0.3
		 *
		 * @var string
		 */
		public $email = '_tribe_tickets_email';

		/**
		 * Meta key that holds the security code that is used for printed tickets and QR codes.
		 *
		 * @since 5.0.3
		 *
		 * @var string
		 */
		public $security_code = '_tribe_tickets_security_code';

		/**
		 * Meta key that holds the price paid for the ticket.
		 *
		 * @since 5.1.0
		 *
		 * @var string
		 */
		public $price_paid = '_paid_price';

		/**
		 * Meta key that holds the price currency symbol used during payment.
		 *
		 * @since 5.1.0
		 *
		 * @var string
		 */
		public $price_currency = '_price_currency_symbol';

		/**
		 * The provider used for Attendees and Tickets ORM.
		 *
		 * @var string
		 */
		public $orm_provider = 'default';

		/**
		 * Meta key that stores if an attendee has checked in to a ticketed post.
		 *
		 * @since 5.8.2
		 *
		 * @var string
		 */
		public $checkin_key = '';

		/**
		 * The key used to store the event ID in the ticket post meta.
		 *
		 * @since 5.14.0
		 *
		 * @var string
		 */
		public $event_key;

		/**
		 * Returns link to the report interface for sales for an event or
		 * null if the provider doesn't have reporting capabilities.
		 *
		 * @abstract
		 *
		 * @param int $post_id ID of parent "event" post
		 * @return mixed
		 */
		public function get_event_reports_link( $post_id ) {}

		/**
		 * Returns link to the report interface for sales for a single ticket or
		 * null if the provider doesn't have reporting capabilities.
		 * As of 4.6 we reversed the params and deprecated $post_id as it was never used
		 *
		 * @abstract
		 *
		 * @param deprecated $post_id ID of parent "event" post
		 * @param int $ticket_id ID of ticket post
		 * @return mixed
		 */
		public function get_ticket_reports_link( $post_id_deprecated, $ticket_id ) {}

		/**
		 * Returns a single ticket.
		 *
		 * @param int $post_id   ID of parent "event" post.
		 * @param int $ticket_id ID of ticket post.
		 *
		 * @return Tribe__Tickets__Ticket_Object|null
		 */
		public function get_ticket( $post_id, $ticket_id ) {
			return null;
		}

		/**
		 * Set the Query args to fetch all the Tickets.
		 *
		 * @since 5.5.0 refactored to use the tickets ORM.
		 * @since 4.6
		 * @since 5.5.2 Set default query args.
		 * @since 5.8.0 Added the `$context` parameter.
		 * @since 5.24.1 Correct the docblock to reflect the method's behavior.
		 *
		 * @param int         $post_id Build the args to query only
		 *                             for tickets related to this post ID.
		 * @param string|null $context The context of the query.
		 *
		 * @return Tribe__Repository__Interface
		 */
		public function set_tickets_query_args( $post_id = null, string $context = null ) {
			$repository = tribe_tickets( $this->orm_provider );
			$repository->set_request_context( $context );
			$repository->by( 'event', $post_id );
			$repository->by( 'status', 'publish' );
			$repository->by( 'posts_per_page', -1 );
			$repository->order_by( 'menu_order' );
			$repository->order( 'ASC' );
			$default_args = $repository->get_query();


			/**
			 * Filters the query arguments that will be used to fetch tickets.
			 *
			 * @since 4.8
			 *
			 * @param array $args
			 */
			$vars = apply_filters( 'tribe_tickets_get_tickets_query_args', $default_args->query_vars );

			if ( $default_args->query_vars !== $vars ) {
				$repository->by_args( $vars );
			}

			return $repository;
		}

		/**
		 * Retrieve the ID numbers of all tickets assigned to an event.
		 *
		 * @since 4.6
		 * @since 5.5.0 refactored to use the tickets ORM.
		 * @since 5.8.0 Added the `$context` parameter.
		 *
		 * @param int|WP_Post $post Only get tickets assigned to this post ID.
		 *
		 * @return array|false
		 */
		public function get_tickets_ids( $post = 0, string $context = null ) {
			$post_id = 0;
			if ( is_numeric( $post ) ) {
				$post_id = (int) $post;
			}

			if ( ! empty( $post ) ) {
				if ( ! $post instanceof WP_Post ) {
					$post = get_post( $post );
				}
				if ( ! $post instanceof WP_Post ) {
					return false;
				}

				if ( class_exists( Occurrence::class, false ) ) {
					/**
					 * Filters the post ID to use when fetching tickets for an Occurrence.
					 *
					 * @since 5.8.0
					 *
					 * @param int $post_id The post ID to use when fetching tickets for an Occurrence; this might
					 *                     be a real post ID, or a provisional one.
					 */
					$post_id = apply_filters( 'tec_tickets_normalize_occurrence_id', Occurrence::normalize_id( $post->ID ) );
				}
			}

			return $this->set_tickets_query_args( $post_id, $context )->get_ids();
		}

		/**
		 * Returns the html for the delete ticket link
		 *
		 * @since 4.6
		 *
		 * @param object $ticket Ticket object
		 *
		 * @return string HTML link
		 */
		public function get_ticket_delete_link( $ticket = null ) {
			if ( empty( $ticket ) ) {
				return '';
			}

			$delete_text = _x( 'Delete %s', 'delete link', 'event-tickets' );

			$button_text = ( 'Tribe__Tickets__RSVP' === $ticket->provider_class )
				? sprintf( $delete_text, tribe_get_rsvp_label_singular( 'delete_link' ) )
				: sprintf( $delete_text, tribe_get_ticket_label_singular( 'delete_link' ) );

			/**
			 * Allows for the filtering and testing if a user can delete tickets
			 *
			 * @since 4.6
			 *
			 * @param bool true
			 * @param int ticket post ID
			 *
			 * @return string HTML link | void HTML link
			 */
			if ( apply_filters( 'tribe_tickets_current_user_can_delete_ticket', true, $ticket->ID, $ticket->provider_class ) ) {
				$delete_link = sprintf(
					'<span><a href="#" attr-provider="%1$s" attr-ticket-id="%2$s" id="ticket_delete_%2$s" class="ticket_delete">%3$s</a></span>',
					$ticket->provider_class,
					$ticket->ID,
					esc_html( $button_text )
				);

				return $delete_link;
			}

			$delete_link = sprintf(
				'<span><a href="#" attr-provider="%1$s" attr-ticket-id="%2$s" id="ticket_delete_%2$s" class="ticket_delete">%3$s</a></span>',
				$ticket->provider_class,
				$ticket->ID,
				esc_html__( $button_text )
			);

			return $delete_link;
		}

		/**
		 * Returns the url for the move ticket link
		 *
		 * @since 4.6
		 *
		 * @param int    $post_id ID of parent "event" post
		 * @param object $ticket  Ticket object
		 *
		 * @return string HTML link | void HTML link
		 */
		public function get_ticket_move_url( $post_id, $ticket = null ) {
			if ( empty( $ticket ) || empty( $post_id ) ) {
				return '';
			}

			$post_url = get_edit_post_link( $post_id, 'admin' );

			$move_type_url = add_query_arg(
				[
					'dialog'         => Tribe__Tickets__Main::instance()->move_ticket_types()->dialog_name(),
					'ticket_type_id' => $ticket->ID,
					'check'          => wp_create_nonce( 'move_tickets' ),
					'TB_iframe'      => 'true',
				],
				$post_url
			);

			return $move_type_url;
		}

		/**
		 * Returns the html for the move ticket link
		 *
		 * @since 4.6
		 *
		 * @param int    $post_id ID of parent "event" post
		 * @param object $ticket  Ticket object
		 *
		 * @return string HTML link | void HTML link
		 */
		public function get_ticket_move_link( $post_id, $ticket = null ) {
			if ( empty( $ticket ) ) {
				return '';
			}

			$move_text = __( 'Move %s', 'event-tickets' );

			$button_text = ( 'Tribe__Tickets__RSVP' === $ticket->provider_class ) ? sprintf( $move_text, tribe_get_rsvp_label_singular( 'move_ticket_button_text' ) ) : sprintf( $move_text, tribe_get_ticket_label_singular( 'move_ticket_button_text' ) ) ;

			$move_url = $this->get_ticket_move_url( $post_id, $ticket );

			if ( empty( $move_url ) ) {
				return '';
			}

			// Make sure Thickbox is available regardless of which admin page we're on.
			add_thickbox();

			$move_link = sprintf( '<a href="%1$s" class="thickbox tribe-ticket-move-link">%2$s</a>', $move_url, esc_html( $button_text ) );

			return $move_link;
		}

		/**
		 * Get the controls (move, delete) as a string and add to our ajax return
		 *
		 * @deprecated 4.6.2
		 * @since 4.6
		 *
		 * @param array $return the ajax return data
		 * @return array $return modified data
		 */
		public function ajax_ticket_edit_controls( $return ) {
			$ticket = $this->get_ticket( $return['post_id'], $return['ID'] );

			if ( empty( $ticket ) ) {
				return $return;
			}

			$controls   = [];

			if ( tribe_is_truthy( tribe_get_request_var( 'is_admin' ) ) ) {
				$controls[] = $this->get_ticket_move_link( $return['post_id'], $ticket );
			}
			$controls[] = $this->get_ticket_delete_link( $ticket );

			if ( ! empty( $controls ) ) {
				$return['controls'] = join( '  |  ', $controls );
			}

			return $return;
		}

		/**
		 * Attempts to load the specified ticket type post object.
		 *
		 * @param int $ticket_id ID of ticket post
		 * @return Tribe__Tickets__Ticket_Object|null
		 */
		public static function load_ticket_object( $ticket_id ) {
			foreach ( self::modules() as $provider_class => $name ) {
				$provider = static::get_ticket_provider_instance( $provider_class );

				if ( empty( $provider ) ) {
					continue;
				}

				$event = $provider->get_event_for_ticket( $ticket_id );

				if ( empty( $event ) ) {
					continue;
				}

				$ticket_object = $provider->get_ticket( $event->ID, $ticket_id );

				if ( $ticket_object ) {
					return $ticket_object;
				}
			}

			return null;
		}

		/**
		 * Returns the event post corresponding to the possible ticket object/ticket ID.
		 *
		 * This is used to help differentiate between products which act as tickets for an
		 * event and those which do not. If $possible_ticket is not related to any events
		 * then boolean false will be returned.
		 *
		 * This stub method should be treated as if it were an abstract method - ie, the
		 * concrete class ought to provide the implementation.
		 *
		 * @param object $ticket_product The ticket product.
		 *
		 * @return bool|WP_Post
		 */
		public function get_event_for_ticket( $ticket_product ) {
			if ( is_object( $ticket_product ) && isset( $ticket_product->ID ) ) {
				$ticket_product = $ticket_product->ID;
			}

			if ( null === get_post( $ticket_product ) ) {
				return false;
			}

			$event_id = get_post_meta( $ticket_product, $this->get_event_key(), true );

			if ( ! empty( $this->attendee_event_key ) && ! $event_id && '' === ( $event_id = get_post_meta( $ticket_product, $this->attendee_event_key, true ) ) ) {
				return false;
			}

			$post_types = Tribe__Tickets__Main::instance()->post_types();
			if ( in_array( get_post_type( $event_id ), $post_types ) ) {
				return get_post( $event_id );
			}

			return false;
		}

		/**
		 * Deletes a ticket
		 *
		 * @abstract
		 *
		 * @param int $post_id ID of parent "event" post
		 * @param int $ticket_id ID of ticket post
		 * @return mixed
		 */
		public function delete_ticket( $post_id, $ticket_id ) {

			/**
			 * Trigger action when any attendee is deleted.
			 *
			 * @since 5.1.5
			 *
			 * @param int $post_id Post or Event ID.
			 * @param int $ticket_id Attendee ID.
			 */
			do_action( 'event_tickets_attendee_ticket_deleted', $post_id, $ticket_id );

			$this->clear_ticket_cache_for_post( $post_id );
			$this->clear_attendees_cache( $post_id );
		}

		/**
		 * Saves a ticket.
		 *
		 * @abstract
		 *
		 * @param int                           $post_id  Post ID.
		 * @param Tribe__Tickets__Ticket_Object $ticket   Ticket object.
		 * @param array                         $raw_data Ticket data.
		 *
		 * @return int|false The updated/created ticket post ID or false if no ticket ID.
		 */
		public function save_ticket( $post_id, $ticket, $raw_data = [] ) {
			$this->clear_ticket_cache_for_post( $post_id );

			return false;
		}

		/**
		 * Whether a post has tickets from this provider, even if this provider is not the default provider.
		 *
		 * @since 4.12.3
		 *
		 * @param int|WP_Post $post
		 *
		 * @return bool True if this post has any tickets from this provider.
		 */
		public function post_has_tickets( $post ) {
			$post_id = Tribe__Main::post_id_helper( $post );

			if ( empty( $post_id ) ) {
				return false;
			}

			return ! empty( $this->get_tickets_ids( $post_id ) );
		}

		/**
		 * Clear the ticket cache for a specific post ID.
		 *
		 * @since 5.1.0
		 *
		 * @param int $post_id The post ID.
		 */
		public function clear_ticket_cache_for_post( $post_id ) {
			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );

			$class = __CLASS__;

			$methods = [
				'get_tickets',
			];

			foreach ( $methods as $method ) {
				$key = $class . '::' . $method . '-' . $this->orm_provider . '-' . $post_id;

				unset( $cache[ $key ] );
			}

			$static_methods = [
				'get_all_event_tickets',
				'get_event_attendees_count',
			];

			foreach ( $static_methods as $method ) {
				$key = $class . '::' . $method . '-' . $post_id;

				unset( $cache[ $key ] );
			}

			$ticket_ids = $this->get_tickets_ids( $post_id );
			foreach ( (array) $ticket_ids as $ticket_id ) {
				clean_post_cache( $ticket_id );
			}
		}

		/**
		 * Returns all the tickets for an event, of the active ticket providers.
		 *
		 * @since 4.12.0 Changed from protected abstract to public with duplicated child classes' logic consolidated here.
		 * @since 5.8.0 Added the `$context` parameter.
		 *
		 * @param int $post_id ID of parent "event" post.
		 * @param string|null $context The context of the request.
		 *
		 * @return Tribe__Tickets__Ticket_Object[] List of ticket objects.
		 */
		public function get_tickets( $post_id, string $context = null ) {

			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$key   = __METHOD__ . '-' . $this->orm_provider . '-' . $post_id;

			if ( isset( $cache[ $key ] ) && is_array( $cache[ $key ] ) ) {
				return $cache[ $key ];
			}

			$default_provider = static::get_event_ticket_provider( $post_id );

			if ( empty( $default_provider ) ) {
				return [];
			}

			// If the post's provider doesn't match.
			if (
				$this->class_name !== $default_provider
				&& ! is_admin()
			) {
				return [];
			}

			$ticket_ids = $this->get_tickets_ids( $post_id, $context );

			if ( ! $ticket_ids ) {
				return [];
			}

			$tickets = [];

			foreach ( $ticket_ids as $post ) {
				$ticket = $this->get_ticket( $post_id, $post );

				if (
					! $ticket instanceof Tribe__Tickets__Ticket_Object
					|| $this->class_name !== $ticket->provider_class
				) {
					continue;
				}

				$tickets[] = $ticket;
			}

			$cache[ $key ] = $tickets;

			return $tickets;
		}

		/**
		 * Get attendees for a Post ID / Post type.
		 *
		 * @param int         $post_id   Post ID.
		 * @param null|string $post_type Post type.
		 *
		 * @return array List of attendees.
		 */
		public function get_attendees_by_id( $post_id, $post_type = null ) {
			return $this->get_attendees_by_post_id( $post_id );
		}

		/**
		 * Get attendees for an event ID.
		 *
		 * @param int $event_id Event post ID.
		 *
		 * @return array List of attendees.
		 */
		protected function get_attendees_by_post_id( $event_id ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			return $this->get_attendees_from_module( $repository->by( 'event', $event_id )->all(), $event_id );
		}

		/**
		 * Get attendees for a ticket ID.
		 *
		 * @since 4.10.6
		 *
		 * @param int $ticket_id Ticket ID.
		 *
		 * @return array List of attendees.
		 */
		protected function get_attendees_by_ticket_id( $ticket_id ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			return $this->get_attendees_from_module( $repository->by( 'ticket', $ticket_id )->all() );
		}

		/**
		 * Get attendees for a ticket ID.
		 *
		 * @since 4.10.6
		 *
		 * @param int $ticket_id Ticket ID.
		 *
		 * @return array List of attendees.
		 */
		protected function get_attendees_by_product_id( $ticket_id ) {
			return $this->get_attendees_by_ticket_id( $ticket_id );
		}

		/**
		 * Get attendees for a ticket by order ID, optionally by ticket ID.
		 *
		 * @since 4.6
		 *
		 * @param int|string $order_id  Order ID.
		 * @param null|int   $ticket_id (optional) Ticket ID.
		 *
		 * @return array List of attendees.
		 */
		public function get_attendees_by_order_id( $order_id ) {
			$ticket_id = null;

			// Support an optional second argument while not causing warnings from other ticket provider classes.
			if ( 1 < func_num_args() ) {
				$ticket_id = func_get_arg( 1 );
			}

			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			$repository->by( 'order', $order_id );

			if ( $ticket_id ) {
				$repository->by( 'ticket', $ticket_id );
			}

			return $this->get_attendees_from_module( $repository->all() );
		}

		/**
		 * Get attendees for a ticket by attendee ID.
		 *
		 * @since 4.6
		 *
		 * @param int $attendee_id Attendee ID.
		 *
		 * @return array List of attendees.
		 */
		protected function get_attendees_by_attendee_id( $attendee_id ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			return $this->get_attendees_from_module( $repository->by( 'id', $attendee_id )->all() );
		}

		/**
		 * Get attendees for a ticket by user ID.
		 *
		 * @since 4.10.6
		 *
		 * @param int $user_id User ID.
		 * @param int $post_id Post or Event ID.
		 *
		 * @return array List of attendees.
		 */
		public function get_attendees_by_user_id( $user_id, $post_id = 0 ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			$repository->by( 'user', $user_id );

			if ( $post_id ) {
				$repository->by( 'event', $post_id );
			}

			return $this->get_attendees_from_module( $repository->all() );
		}

		/**
		 * Get All Attendees by ticket/attendee ID
		 *
		 * @since 4.8.0
		 *
		 * @param int $attendee_id
		 * @return array
		 */
		public function get_all_attendees_by_attendee_id( $attendee_id ) {
			return $this->get_attendees_by_attendee_id( $attendee_id );
		}

		/**
		 * Get attendees from provided query
		 *
		 * @param WP_Query $attendees_query
		 * @param int $post_id ID of parent "event" post
		 * @return mixed
		 */
		protected function get_attendees( $attendees_query, $post_id ) {
			$attendees = [];

			foreach ( $attendees_query->posts as $attendee ) {
				$attendee_data = $this->get_attendee( $attendee, $post_id );

				if ( ! $attendee_data ) {
					continue;
				}

				$attendees[] = $attendee_data;
			}

			return $attendees;
		}

		/**
		 * Whether a specific attendee is valid toward inventory decrease or not.
		 *
		 * @since 4.7
		 *
		 * @param array $attendee
		 *
		 * @return bool
		 */
		public function attendee_decreases_inventory( array $attendee ) {
			return true;
		}

		/**
		 * Handles if email sending is allowed.
		 *
		 * @since 5.2.1
		 *
		 * @param WP_Post|null $ticket   The ticket post object if available, otherwise null.
		 * @param array|null   $attendee The attendee information if available, otherwise null.
		 *
		 *  @return boolean
		 */
		public function allow_resending_email( $ticket = null, $attendee = null ) {
			/**
			 *
			 * Shared filter between Woo, EDD, and the default logic.
			 * This filter allows the admin to control the re-send email option when an attendee's email is updated per a payment type (EDD, Woo, etc).
			 * True means allow email resend, false means disallow email resend.
			 *
			 * @since 5.2.1
			 *
			 * @param WP_Post|null $ticket The ticket post object if available, otherwise null.
			 * @param array|null $attendee The attendee information if available, otherwise null.
			 *
			 */
			return (bool) apply_filters( 'tribe_tickets_my_tickets_allow_email_resend_on_attendee_email_update', true, $ticket, $attendee );
		}

		/**
		 * Mark an Attendee as checked in.
		 *
		 * @since 3.1.2
		 * @since 5.8.2 Add the `tec_tickets_attendee_checkin` filter to override the checkin process. Update the method
		 *        signature to include the `$qr` and `$eveent_id` parameters.
		 *
		 * @param int       $attendee_id The ID of the attendee that's being checked in.
		 * @param bool|null $qr          Whether the check-in comes from a QR code scan or not.
		 * @param int|null  $event_id    The ID of the ticket-able post the Attendee is being checked into.
		 *
		 * @return bool Whether the Attendee was checked in or not.
		 */
		public function checkin( $attendee_id, $qr = null, $event_id = null ) {
			/**
			 * Allows filtering the Attendee check-in action before the default logic does it.
			 * Returning a non-null value from this filter will prevent the default logic from running.
			 *
			 * @since 5.8.2
			 *
			 * @param int      $attendee_id The post ID of the Attendee being checked-in.
			 * @param int|null $event_id    The ID of the ticket-able post the Attendee is being checked into.
			 * @param bool     $qr          Whether the check-in comes from a QR code scan or not.
			 */
			$checkin = apply_filters( 'tec_tickets_attendee_checkin', null, (int) $attendee_id, (int) $event_id, (bool) $qr );
			if ( $checkin !== null ) {
				return (bool) $checkin;
			}

			update_post_meta( $attendee_id, $this->checkin_key, 1 );

			if ( isset( $qr ) && $qr = (bool) $qr ) {
				update_post_meta( $attendee_id, '_tribe_qr_status', 1 );
			}

			$this->save_checkin_details( $attendee_id, $qr );

			/**
			 * Fires a checkin action
			 *
			 * @since 4.7
			 * @since 5.8.2 Add the `$event_id` argument to the filter data.
			 *
			 * @param int       $attendee_id he post ID of the attendee that's being checked-in.
			 * @param bool|null $qr          Whether the check-in is from a QR code.
			 * @param int|null  $event_id    The ID of the ticket-able post the Attendee is being checked into.
			 */
			do_action( 'event_tickets_checkin', $attendee_id, $qr, $event_id );

			return true;
		}

		/**
		 * Save the attendee checkin details.
		 *
		 * @since 5.5.2
		 * @since 5.5.11 Update attendee scan count via tec_tickets_plus_app_attendees_checked_in option.
		 *
		 * @param int   $attendee_id     The ID of the attendee that's being checked-in.
		 * @param mixed $qr              True if the check-in is from a QR code.
		 */
		public function save_checkin_details( $attendee_id, $qr ) {
			$checkin_details = [
				'date'   => current_time( 'mysql' ),
				'source' => ! empty( $qr ) ? 'app' : 'site',
				'author' => get_current_user_id(),
			];

			if ( ! empty( $qr ) ) {
				// Save the latest date in which a ticket was scanned with the APP.
				tribe_update_option( 'tec_tickets_plus_app_last_checkin_time', time() );

				// Save the attendee scan count.
				$attendee_scan_count = (int) tribe_get_option( 'tec_tickets_plus_app_attendees_checked_in' );
				tribe_update_option( 'tec_tickets_plus_app_attendees_checked_in', ++$attendee_scan_count );
			}

			/**
			 * Filters the checkin details for this attendee checkin.
			 *
			 * @since 5.5.2
			 *
			 * @param array $checkin_details The check-in details.
			 * @param int   $attendee_id     The ID of the attendee that's being checked-in.
			 * @param mixed $qr              True if the check-in is from a QR code.
			 */
			$checkin_details = apply_filters( 'tec_tickets_checkin_details', $checkin_details, $attendee_id, $qr );

			update_post_meta( $attendee_id, $this->checkin_key . '_details', $checkin_details );
		}

		/**
		 * Mark an attendee as not checked in
		 *
		 * @abstract
		 *
		 * @param int $attendee_id
		 * @return mixed
		 */
		public function uncheckin( $attendee_id ) {
			$context_id = tribe_get_request_var( 'event_ID', null );

			/**
			 * Allows filtering the Attendee uncheck-in action before the default logic does it.
			 * Returning a non-null value from this filter will prevent the default logic from running.
			 *
			 * @since 5.8.3
			 *
			 * @param bool|null $uncheckin   Whether the Attendee uncheckin action was handled by the filter or not.
			 * @param int       $attendee_id The post ID of the Attendee being unchecked-in.
			 * @param int|null  $context_id  The post ID context of the Attendee uncheckin request.
			 */
			$uncheckin = apply_filters( 'tec_tickets_attendee_uncheckin', null, $attendee_id, $context_id );

			if ( null !== $uncheckin ) {
				return (bool) $uncheckin;
			}

			delete_post_meta( $attendee_id, $this->checkin_key );
			delete_post_meta( $attendee_id, $this->checkin_key . '_details' );
			delete_post_meta( $attendee_id, '_tribe_qr_status' );

			/**
			 * Fires an uncheckin action
			 *
			 * @since 4.7
			 *
			 * @param int $attendee_id
			 */
			do_action( 'event_tickets_uncheckin', $attendee_id );

			return true;
		}

		/**
		 * Renders the advanced fields in the new/edit ticket form.
		 * Using the method, providers can add as many fields as
		 * they want, specific to their implementation.
		 *
		 * @abstract
		 *
		 * @param int $post_id ID of parent "event" post
		 * @param int $ticket_id ID of ticket post
		 * @return mixed
		 */
		public function do_metabox_capacity_options( $post_id, $ticket_id ) {}

		/**
		 * Renders the front end form for selling tickets in the event single page
		 *
		 * @param string $content The content.
		 *
		 * @return string|null
		 */
		public function front_end_tickets_form( $content ) {}

		/**
		 * Returns the markup for the price field
		 * (it may contain the user selected currency, etc)
		 *
		 * @param object|int $product
		 * @param array|boolean $attendee
		 *
		 * @return string
		 */
		public function get_price_html( $product, $attendee = false ) {
			return '';
		}

		/**
		 * Indicates if the module/ticket provider supports a concept of global stock.
		 *
		 * For backward compatibility reasons this method has not been declared abstract but
		 * implementaions are still expected to override it.
		 *
		 * @return bool
		 */
		public function supports_global_stock() {
			return false;
		}

		/**
		 * Returns class instance. Child classes must overload this.
		 *
		 * @static
		 *
		 * @return static
		 */
		public static function get_instance() {}

		// end API Definitions

		/**
		 *
		 */
		public function __construct() {
			// As this is an abstract class, we want to know which child instantiated it
			$this->class_name = $this->className = get_class( $this );

			$this->parent_path = $this->parentPath = trailingslashit( dirname( dirname( dirname( __FILE__ ) ) ) );
			$this->parent_url  = $this->parentUrl  = trailingslashit( plugins_url( '', $this->parent_path ) );

			// Register all Tribe__Tickets__Tickets api consumers
			self::$active_modules[ $this->class_name ] = $this->plugin_name;

			add_action( 'wp', [ $this, 'hook' ] );

			/**
			 * Priority set to 11 to force a specific display order
			 *
			 * @since 4.6
			 */
			add_action( 'tribe_events_tickets_metabox_edit_main', [ $this, 'do_metabox_capacity_options' ], 11, 2 );

			// Ensure ticket prices and event costs are linked
			add_filter( 'tribe_events_event_costs', [ $this, 'get_ticket_prices' ], 10, 2 );
			add_filter( 'tribe_get_event_meta', [ $this, 'exclude_past_tickets_from_cost_range' ], 10, 4 );

			add_action( 'event_tickets_checkin', [ $this, 'purge_attendees_transient' ] );
			add_action( 'event_tickets_uncheckin', [ $this, 'purge_attendees_transient' ] );
			add_action( 'template_redirect', [ $this, 'maybe_redirect_to_attendees_registration_screen' ], 0 );

			// Event cost may need to be formatted to the provider's currency settings.
			add_filter( 'tribe_currency_cost', [ $this, 'maybe_format_event_cost' ], 10, 2 );

			add_action( 'init', [ $this, 'add_admin_tickets_hooks' ] );
		}

		/**
		 * Most Commerce Providers needs this to be setup later than when the actual class is actually loaded
		 *
		 * For Frontend Hooks, admin ones need to be loaded earlier
		 *
		 * @since 4.7.5
		 *
		 * @return void
		 */
		public function hook() {
			// Front end
			$ticket_form_hook = $this->get_ticket_form_hook();

			if ( ! empty( $ticket_form_hook ) ) {
				add_action( $ticket_form_hook, [ $this, 'maybe_add_front_end_tickets_form' ], 5 );
				add_filter( $ticket_form_hook, [ $this, 'show_tickets_unavailable_message' ], 6 );
			}

			add_filter( 'the_content', [ $this, 'front_end_tickets_form_in_content' ], 11 );
			add_filter( 'the_content', [ $this, 'show_tickets_unavailable_message_in_content' ], 12 );
			/**
			 * Trigger an action every time a new ticket instance has been created
			 *
			 * @since 4.9
			 *
			 * @param Tribe__Tickets__Tickets $ticket_handler
			 */
			do_action( 'tribe_tickets_tickets_hook', $this );
		}

		/**
		 * Add all the hooks for the Admin Tickets page.
		 *
		 * @since 5.14.0
		 *
		 * @return void
		 */
		public function add_admin_tickets_hooks() {
			add_filter( 'tec_tickets_admin_tickets_table_provider_info', [ $this, 'filter_admin_tickets_table_provider_info' ] );
		}

		/**
		 * Remove the attendees transient when a Ticket change its state
		 *
		 * @since 4.7.4
		 *
		 * @param int $attendee_id
		 * @return void
		 */
		public function purge_attendees_transient( $attendee_id ) {

			$event_id = $this->get_event_id_from_attendee_id( $attendee_id );

			if ( $event_id ) {
				tribe( 'post-transient' )->delete( $event_id, self::ATTENDEES_CACHE );
			}
		}

		/**
		 * Maybe add the Tickets Form as shouldn't be added if is unchecked from the settings
		 *
		 * @since 4.7.3
		 *
		 * @param string $content
		 */
		public function maybe_add_front_end_tickets_form( $content ) {
			if ( ! tribe_tickets_post_type_enabled( get_post_type() ) ) {
				return;
			}

			if ( post_password_required( get_the_ID() ) ) {
				return;
			}

			return $this->front_end_tickets_form( $content );
		}

		// start Attendees

		/**
		 * Returns all the attendees for an event. Queries all registered providers.
		 *
		 * @static
		 *
		 * @param int   $post_id ID of parent "event" post.
		 * @param array $args    List of arguments to filter by.
		 *
		 * @return array List of attendees.
		 */
		public static function get_event_attendees( $post_id, $args = [] ) {
			$attendees = [];

			/**
			 * Filter to skip all empty $post_ID otherwise will fallback to the current global post ID
			 *
			 * @since 4.9
			 * @since 4.10.6 Added $args parameter.
			 *
			 * @param bool  $skip_empty_post If the empty post should be skipped or not
			 * @param int   $post_id         ID of the post being affected
			 * @param array $args            List of arguments to filter by.
			 */
			$skip_empty_post = apply_filters( 'tribe_tickets_event_attendees_skip_empty_post', true, $post_id, $args );

			/**
			 * Process an attendee only if:
			 *
			 * - $skip_empty_post is true and $post_id is not empty => ( true && false ) => ! false => true
			 * - $skip_empty_post is false and $post_id is empty => ( false && true ) => ! false => true
			 * - $skip_empty_post is false and $post_id is not empty => ( false && false ) => ! false => true
			 *
			 * Is not executed if:
			 *
			 * - $skip_empty_post is true and $post_id is empty => ( true && true ) => ! true => false
			 */
			if ( ! ( $skip_empty_post && empty( $post_id ) ) ) {
				/**
				 * Filters the cache expiration when this function is called from an admin screen.
				 *
				 * Returning a falsy value here will force a fetch each time.
				 *
				 * @since 4.7
				 * @since 4.10.6 Added $args parameter.
				 *
				 * @param int   $admin_expire The cache expiration in seconds; defaults to 2 minutes.
				 * @param int   $post_id      The ID of the post attendees are being fetched for.
				 * @param array $args         List of arguments to filter by.
				 */
				$admin_expire = apply_filters( 'tribe_tickets_attendees_admin_expire', 120, $post_id, $args );

				/**
				 * Filters the cache expiration when this function is called from a non admin screen.
				 *
				 * Returning a falsy value here will force a refetch each time.
				 *
				 * @since 4.7
				 * @since 4.10.6 Added $args parameter.
				 *
				 * @param int   $admin_expire The cache expiration in seconds, defaults to an hour.
				 * @param int   $post_id      The ID of the post attendees are being fetched for.
				 * @param array $args         List of arguments to filter by.
				 */
				$expire = apply_filters( 'tribe_tickets_attendees_expire', HOUR_IN_SECONDS, $post_id, $args );

				$expire = is_admin() ? (int) $admin_expire : (int) $expire;

				$attendees_from_cache = false;

				$post_transient = null;

				$cache_key = false;

				if ( empty( $args ) && 0 < $post_id ) {
					$cache_key = (int) $post_id;
				}

				if ( 0 !== $expire && $cache_key ) {
					/** @var Tribe__Post_Transient $post_transient */
					$post_transient = tribe( 'post-transient' );

					$attendees_from_cache = $post_transient->get( $cache_key, self::ATTENDEES_CACHE );

					// if there is a valid transient, we'll use the value from that and note
					// that we have fetched from cache
					if ( false !== $attendees_from_cache ) {
						$attendees            = empty( $attendees_from_cache ) ? [] : $attendees_from_cache;
						$attendees_from_cache = true;
					}
				}

				// if we haven't grabbed attendees from cache, then attempt to fetch attendees
				if ( false === $attendees_from_cache && empty( $attendees ) ) {
					$attendee_data = self::get_event_attendees_by_args( $post_id, $args );

					if ( ! empty( $attendee_data['attendees'] ) ) {
						$attendees = $attendee_data['attendees'];
					}

					if ( 0 !== $expire && $cache_key ) {
						$post_transient->set( $cache_key, self::ATTENDEES_CACHE, $attendees, $expire );
					}
				}
			}

			/**
			 * Filters the return data for event attendees.
			 *
			 * @since 4.4
			 * @since 4.10.6 Added $args parameter.
			 *
			 * @param array $attendees Array of event attendees.
			 * @param int   $post_id   Event post ID.
			 * @param array $args      List of arguments to filter by.
			 */
			return apply_filters( 'tribe_tickets_event_attendees', $attendees, $post_id, $args );
		}

		/**
		 * Returns all the attendees for an event with filtered by arguments. Queries all registered providers.
		 *
		 * @since 4.10.6
		 * @since 5.5.9 Move the logic to `get_attendees_by_args` and use the method to return the attendees.
		 *
		 * @static
		 *
		 * @param int   $post_id ID of parent "event" post.
		 * @param array $args {
		 *      List of arguments to filter attendees by.
		 *
		 *      @type boolean $return_total_found Whether to return total_found count in an array along with list of
		 *                                        attendees. Default is off.
		 *      @type int     $page               Page number of attendees to return. Default is page 1.
		 *      @type int     $per_page           How many attendees to return per page. Default is all.
		 *      @type string  $fields             Which fields to return. Default is all.
		 *      @type array   $by                 List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
		 *                                        [[what,args...]] format.
		 *      @type array   $where_multi        List of ORM->where_multi() filters to use. [[what,args...]] format.
		 * }
		 *
		 * @return array List of attendees and total_found.
		 */
		public static function get_event_attendees_by_args( $post_id, $args = [] ) {
			$attendee_data = [
				'total_found' => 0,
				'attendees'   => [],
			];

			if ( empty( $post_id ) ) {
				return $attendee_data;
			}

			return self::get_attendees_by_args( $args, $post_id );
		}

		/**
		 * Returns all the attendees with filtered by arguments. Queries all registered providers.
		 *
		 * @since 5.5.9
		 *
		 * @static
		 *
		 * @param int   $post_id ID of parent "event" post.
		 * @param array $args {
		 *      List of arguments to filter attendees by.
		 *
		 *      @type boolean $return_total_found Whether to return total_found count in an array along with list of
		 *                                        attendees. Default is off.
		 *      @type int     $page               Page number of attendees to return. Default is page 1.
		 *      @type int     $per_page           How many attendees to return per page. Default is all.
		 *      @type string  $fields             Which fields to return. Default is all.
		 *      @type array   $by                 List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
		 *                                        [[what,args...]] format.
		 *      @type array   $where_multi        List of ORM->where_multi() filters to use. [[what,args...]] format.
		 * }
		 *
		 * @return array List of attendees and total_found.
		 */
		public static function get_attendees_by_args( $args = [], $post_id = 0 ) {
			$attendee_data = [
				'total_found' => 0,
				'attendees'   => [],
			];

			$provider = 'default';

			if ( ! empty( $args['provider'] ) ) {
				$provider = $args['provider'];
			}

			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $provider );

			// Limit by post ID.
			if ( ! empty( $post_id ) ) {
				$repository->by( 'event', $post_id );
			}

			self::pass_args_to_repository( $repository, $args );

			if ( ! empty( $args['return_total_found'] ) ) {
				$repository->set_found_rows( true );
			}

			$attendee_posts = $repository->all();

			if ( ! empty( $args['return_total_found'] ) ) {
				$attendee_data['total_found'] = $repository->found();
			}

			$attendee_data['attendees'] = self::get_attendees_from_modules( $attendee_posts, $post_id );

			return $attendee_data;
		}

		/**
		 * Pass arguments to repository object with dynamic support for by() and where_multi().
		 *
		 * @since 4.10.6
		 *
		 * @param Tribe__Repository $repository Repository object.
		 * @param array             $args       {
		 *      List of arguments to filter by.
		 *
		 *      @type int     $page               Page number of results to return. Default is page 1.
		 *      @type int     $per_page           How many results to return per page. Default is all.
		 *      @type string  $fields             Which fields to return. Default is all.
		 *      @type array   $by                 List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
		 *                                        [[what,args...]] format.
		 *      @type array   $where_multi        List of ORM->where_multi() filters to use. [[what,args...]] format.
		 * }
		 */
		protected static function pass_args_to_repository( $repository, $args ) {
			// Only return specific fields.
			if ( ! empty( $args['fields'] ) ) {
				$repository->fields( $args['fields'] );
			}

			// Handle filtering.
			if ( ! empty( $args['by'] ) ) {
				foreach ( $args['by'] as $by => $by_args ) {
					$by_args = (array) $by_args;

					if ( is_string( $by ) ) {
						array_unshift( $by_args, $by );
					}

					call_user_func_array( [ $repository, 'by' ], $by_args );
				}
			}

			// Handle post__in.
			if ( ! empty( $args['in'] ) ) {
				$repository->in( (array) $args['in'] );
			}

			// Handle post__not_in.
			if ( ! empty( $args['not_in'] ) ) {
				$repository->not_in( (array) $args['not_in'] );
			}

			// Handle multi filtering.
			if ( ! empty( $args['where_multi'] ) ) {
				foreach ( $args['where_multi'] as $where_multi_args ) {
					call_user_func_array( [ $repository, 'where_multi' ], $where_multi_args );
				}
			}

			// Set current page.
			if ( ! empty( $args['page'] ) ) {
				$repository->page( absint( $args['page'] ) );
			}

			// Limit results per page.
			if ( ! empty( $args['per_page'] ) ) {
				$repository->per_page( absint( $args['per_page'] ) );
			}

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

			if ( ! empty( $args['order'] ) ) {
				$repository->order( strval( $args['order'] ) );
			}
		}

		/**
		 * Get attendee data for attendees from the associated modules.
		 *
		 * @since 4.10.6
		 *
		 * @param array $attendees Attendee objects or IDs.
		 * @param int   $post_id   Parent post ID.
		 *
		 * @return array The attendee data for attendees.
		 */
		public static function get_attendees_from_modules( $attendees, $post_id = 0 ) {
			$attendees_from_modules = [];

			foreach ( $attendees as $attendee ) {
				/** @var Tribe__Tickets__Tickets $provider */
				$provider = tribe_tickets_get_ticket_provider( $attendee );

				// Could be `false`, such as ticket for a disabled commerce provider.
				if ( empty( $provider ) ) {
					continue;
				}

				$attendee_data = $provider->get_attendee( $attendee, $post_id );

				if ( ! $attendee_data ) {
					continue;
				}

				// Set the `ticket_exists` flag on attendees if the ticket they are associated with does not exist.
				$attendee_data['ticket_exists'] = ! empty( $attendee_data['product_id'] ) && get_post( $attendee_data['product_id'] );

				// Set the ticket type from the ticket oject, if possible.
				$attendee_data['ticket_type'] = 'default';
				if ( isset( $attendee_data['event_id'], $attendee_data['product_id'] )
				     && $ticket = $provider->get_ticket( $attendee_data['event_id'], $attendee_data['product_id'] ) ) {
					$attendee_data['ticket_type'] = $ticket->type();
				}

				$attendees_from_modules[] = $attendee_data;
			}

			return $attendees_from_modules;
		}

		/**
		 * Get attendee data for attendees from the current module.
		 *
		 * @since 4.10.6
		 *
		 * @param array $attendees Attendee objects or IDs.
		 * @param int   $post_id   Parent post ID.
		 *
		 * @return array The attendee data for attendees.
		 */
		public function get_attendees_from_module( $attendees, $post_id = 0 ) {
			$attendees_from_module = [];

			foreach ( $attendees as $attendee ) {
				$attendee_data = $this->get_attendee( $attendee, $post_id );

				if ( ! $attendee_data ) {
					continue;
				}

				// Set the `ticket_exists` flag on attendees if the ticket they are associated with does not exist.
				$attendee_data['ticket_exists'] = ! empty( $attendee_data['product_id'] ) && get_post( $attendee_data['product_id'] );

				$attendees_from_module[] = $attendee_data;
			}

			return $attendees_from_module;
		}

		/**
		 * Get attendee data for attendee.
		 *
		 * @since 4.10.6
		 *
		 * @param WP_Post|int $attendee Attendee object or ID.
		 * @param int         $post_id  Parent post ID.
		 *
		 * @return array|false The attendee data or false if the ticket is invalid.
		 */
		public function get_attendee( $attendee, $post_id = 0 ) {
			return false;
		}

		/**
		 * Returns an array of attendees for the specified event, in relation to
		 * this ticketing provider.
		 *
		 * @param int $post_id ID of parent "event" post
		 * @return array
		 */
		public function get_attendees_array( $post_id ) {
			return $this->get_attendees_by_post_id( $post_id );
		}

		/**
		 * Returns total count of attendees for the specified event, in relation to
		 * this ticketing provider.
		 *
		 * @since 4.10.6
		 *
		 * @param int $post_id ID of parent "event" post
		 *
		 * @return int Total count of attendees.
		 */
		public function get_attendees_count( $post_id ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			return $repository->by( 'event', $post_id )->found();
		}

		/**
		 * Returns total count of attendees for the specified event, in relation to
		 * this ticketing provider.
		 *
		 * @since 4.10.6
		 *
		 * @param int $post_id ID of parent "event" post.
		 * @param int $user_id ID of user.
		 *
		 * @return int Total count of attendees.
		 */
		public function get_attendees_count_by_user( $post_id, $user_id ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $this->orm_provider );

			return $repository->by( 'event', $post_id )->by( 'user', $user_id )->found();
		}

		/**
		 * Returns the total number of attendees for an event (regardless of provider).
		 *
		 * @param int   $post_id ID of parent "event" post.
		 * @param array $args    {
		 *      List of arguments to filter attendees by.
		 *
		 *      @type array $by          List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
		 *                               [[what,args...]] format.
		 *      @type array $where_multi List of ORM->where_multi() filters to use. [[what,args...]] format.
		 * }
		 *
		 * @return int Total count of attendees.
		 */
		public static function get_event_attendees_count( $post_id, $args = [] ) {
			// Post ID is required.
			if ( empty( $post_id ) ) {
				return 0;
			}

			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$key   = __METHOD__ . '-' . $post_id;

			if ( empty( $args ) && isset( $cache[ $key ] ) ) {
				return $cache[ $key ];
			}

			$provider = 'default';

			if ( ! empty( $args['provider'] ) ) {
				$provider = $args['provider'];
			}

			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees( $provider );

			$repository->by( 'event', $post_id );

			self::pass_args_to_repository( $repository, $args );

			$found = $repository->found();

			if ( empty( $args ) ) {
				$cache[ $key ] = $found;
			}

			return $found;
		}

		/**
		 * Returns all tickets for an event (all providers are queried for this information).
		 *
		 * @since 5.8.0 Added the `$context` parameter.
		 *
		 * @param int $post_id ID of parent "event" post
		 * @param string|null $context The context of the request.
		 *
		 * @return array
		 */
		public static function get_all_event_tickets( $post_id, string $context = null ) {

			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$key   = __METHOD__ . '-' . $post_id;

			if ( is_array( $cache[ $key ] ) ) {
				return $cache[ $key ];
			}

			$tickets = [];
			$modules = self::modules();

			foreach ( $modules as $class => $module ) {
				$obj              = call_user_func( [ $class, 'get_instance' ] );
				$provider_tickets = $obj->get_tickets( $post_id, $context );
				if ( is_array( $provider_tickets ) && ! empty( $provider_tickets ) ) {
					$tickets[] = $provider_tickets;
				}
			}

			$tickets = empty( $tickets ) ? [] : call_user_func_array( 'array_merge', $tickets );
			$cache[ $key ] = $tickets;

			return $tickets;
		}

		/**
		 * Tests to see if the provided object/ID functions as a ticket for the event
		 * and returns the corresponding event if so (or else boolean false).
		 *
		 * All registered providers are asked to perform this test.
		 *
		 * @param object|int $possible_ticket
		 *
		 * @return WP_Post|false
		 */
		public static function find_matching_event( $possible_ticket ) {
			foreach ( self::modules() as $class => $module ) {
				$obj   = call_user_func( [ $class, 'get_instance' ] );
				$event = $obj->get_event_for_ticket( $possible_ticket );
				if ( $event instanceof WP_Post ) {
					return $event;
				}
			}

			return false;
		}

		/**
		 * Returns the sum of all checked-in attendees for an event. Queries all registered providers.
		 *
		 * @static
		 *
		 * @param int $post_id ID of parent "event" post
		 * @return mixed
		 */
		final public static function get_event_checkedin_attendees_count( $post_id ) {
			/** @var Tribe__Tickets__Attendee_Repository $repository */
			$repository = tribe_attendees();

			return $repository->by( 'event', $post_id )->by( 'checkedin', true )->found();
		}

		// end Attendees

		// start Helpers

		/**
		 * Indicates if any of the currently available providers support global stock.
		 *
		 * @return bool
		 */
		public static function global_stock_available() {
			foreach ( self::modules() as $class => $module ) {
				$provider = call_user_func( [ $class, 'get_instance' ] );

				if ( method_exists( $provider, 'supports_global_stock' ) && $provider->supports_global_stock() ) {
					return true;
				}
			}

			return false;
		}

		/**
		 * Echos the class for the <tr> in the tickets list admin
		 */
		protected function tr_class() {
			echo 'ticket_advanced_' . sanitize_html_class( $this->class_name );
		}

		/**
		 * Generates a set of radio buttons listing the available global stock mode options.
		 *
		 * @param string (empty string) $current_option
		 * @return string
		 */
		protected function global_stock_mode_selector( $current_option = '' ) {
			$output = "<fieldset id='ticket_global_stock' class='input_block' >";
			$output .= "<legend class='ticket_form_label'>Capacity:</legend>";

			// Default to using own stock unless the user explicitly specifies otherwise (important
			// to avoid assuming global stock mode if global stock is enabled/disabled accidentally etc)
			if ( empty( $current_option ) ) {
				$current_option = Tribe__Tickets__Global_Stock::OWN_STOCK_MODE;
			}

			foreach ( $this->global_stock_mode_options() as $identifier => $name ) {
				$output .= '<label for="' . esc_attr( $identifier ) . '" class="ticket_field"><input type="radio" id="' . esc_attr( $identifier ) . '" class=" name="ticket_global_stock" value="' . esc_attr( $identifier ) . '" ' . selected( $identifier === $current_option ) . '> ' . esc_html( $name ) . " </label>\n";
			}

			return $output;
		}

		/**
		 * Returns an array of standard stock mode options that can be reused by implementations.
		 *
		 * Format is: ['identifier' => 'Localized name', ... ]
		 *
		 * @return array
		 */
		protected function global_stock_mode_options() {
			return [
				Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE => __( 'Shared capacity with other tickets', 'event-tickets' ),
				Tribe__Tickets__Global_Stock::OWN_STOCK_MODE    => __( 'Set capacity for this ticket only', 'event-tickets' ),
			];
		}

		/**
		 * Get JS localize data for ticket options.
		 *
		 * @since 4.11.0.1
		 *
		 * @return array JS localize data for ticket options.
		 */
		public static function get_asset_localize_data_for_ticket_options() {
			$availability_check_interval = MINUTE_IN_SECONDS * 1000;

			/*
			 * Prevent availability check AJAX errors because we don't currently
			 * run our AJAX hook if this conditional fails.
			 *
			 * A temporary fix for ET-730 which will need to be followed up with.
			 *
			 * @see \Tribe__Tickets__Editor__Provider::register()
			 * @see \Tribe__Tickets__Editor__Blocks__Tickets::hook()
			 */
			if ( ! tribe( 'editor' )->should_load_blocks() ) {
				$availability_check_interval = 0;
			}

			/**
			 * Allow filtering how often tickets availability is checked (in milliseconds).
			 *
			 * @since 4.11.0
			 *
			 * @param int $availability_check_interval How often to check availability for tickets (in milliseconds).
			 */
			$availability_check_interval = apply_filters( 'tribe_tickets_availability_check_interval', $availability_check_interval );

			$post_id = get_the_ID();

			if ( empty( $post_id ) && get_queried_object() instanceof WP_Post ) {
				$post_id = get_queried_object_id();
			}

			return [
				'post_id'                     => $post_id,
				'ajaxurl'                     => admin_url( 'admin-ajax.php', ( is_ssl() ? 'https' : 'http' ) ),
				'availability_check_interval' => $availability_check_interval,
			];
		}

		/**
		 * Get JS localize data for currencies.
		 *
		 * @since 4.11.0.1
		 *
		 * @return array JS localize data for currencies.
		 */
		public static function get_asset_localize_data_for_currencies() {
			/** @var Tribe__Tickets__Commerce__Currency $currency */
			$currency = tribe( 'tickets.commerce.currency' );

			$currencies = $currency->get_currency_config_for_providers();

			return [
				'formatting' => json_encode( $currencies ),
			];
		}

		/**
		 * Get JS localize data for cart/checkout URLs.
		 *
		 * @since 4.11.0.1
		 *
		 * @return array JS localize data for cart/checkout URLs.
		 */
		public static function get_asset_localize_data_for_cart_checkout_urls() {
			$cart_urls     = [];
			$checkout_urls = [];

			/**
			 * Allow providers to add their own checkout URL to the localized list.
			 *
			 * @since 4.11.0
			 *
			 * @param array $checkout_urls An array to add urls to.
			 */
			$checkout_urls = apply_filters( 'tribe_tickets_checkout_urls', $checkout_urls );

			/**
			 * Allow providers to add their own cart URL to the localized list.
			 *
			 * @since 4.11.0
			 *
			 * @param array $cart_urls An array to add urls to.
			 */
			$cart_urls = apply_filters( 'tribe_tickets_cart_urls', $cart_urls );

			return [
				'cart'     => $cart_urls,
				'checkout' => $checkout_urls,
			];
		}

		/**
		 * Get RSVP and Ticket counts for an event if tickets are currently available.
		 *
		 * @param int $post_id ID of parent "event" post
		 *
		 * @return array
		 */
		public static function get_ticket_counts( $post_id ) {
			// if no post id return empty array
			if ( empty( $post_id ) ) {
				return [];
			}

			$tickets = self::get_all_event_tickets( $post_id );

			// if no tickets or rsvp return empty array
			if ( ! $tickets ) {
				return [];
			}

			/**
			 * This order is important so that tickets overwrite RSVP on
			 * the Buy Now Button on the front-end
			 */
			$types['rsvp']    = [
				'count'     => 0,
				'stock'     => 0,
				'unlimited' => 0,
				'available' => 0,
			];
			$types['tickets'] = [
				'count'     => 0, // count of ticket types currently for sale
				'stock'     => 0, // current stock of tickets available for sale
				'global'    => 0, // numeric boolean if tickets share global stock
				'unlimited' => 0, // numeric boolean if any ticket has unlimited stock
				'available' => 0,
			];

			/** @var Tribe__Tickets__Ticket_Object $ticket */
			foreach ( $tickets as $ticket ) {
				// If a ticket is not current for sale do not count it
				if ( ! tribe_events_ticket_is_on_sale( $ticket ) ) {
					continue;
				}

				if ( 'Tribe__Tickets__RSVP' === $ticket->provider_class ) {
					$types = static::process_rsvp_counts( $ticket, $types );
					continue;
				}

				// we have a ticket type so increasing the ticket count.
				$types['tickets']['count'] ++;

				$global_stock_mode = $ticket->global_stock_mode();

				// Handle tickets with unlimited capacity.
				if ( empty( $global_stock_mode ) ) {
					if ( ! $ticket->manage_stock() || -1 === $ticket->capacity ) {
						$types['tickets']['unlimited'] ++;
						$types['tickets']['available'] ++;
					}
					continue;
				}

				// for individual tickets.
				if ( Tribe__Tickets__Global_Stock::OWN_STOCK_MODE === $global_stock_mode ) {
					$stock_level = $ticket->available();
					$types['tickets']['stock'] += $stock_level;
					$types['tickets']['available'] += $stock_level;
					if ( ! $ticket->manage_stock() || -1 === $ticket->capacity ) {
						$types['tickets']['unlimited'] ++;
					}
					continue;
				}

				// flag if we have any shared capacity tickets.
				if ( Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE === $global_stock_mode ) {
					$types['tickets']['global'] = 1;
					continue;
				}

				$stock_level = Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE === $global_stock_mode ? $ticket->global_stock_cap() : $ticket->available();

				// whether the stock level is negative because it represents unlimited stock (`-1`)
				// or because it's oversold we normalize to `0` for the sake of displaying
				$stock_level = max( 0, (int) $stock_level );

				$types['tickets']['stock'] += $stock_level;

				// If current availability is unlimited (available = -1) and the ticket has stock, set it to 0.
				if ( $types['tickets']['available'] < 0 && 0 !== $types['tickets']['stock'] ) {
					$types['tickets']['available'] = 0;
				}
			}

			/*
			 * The Tickets that should be displayed on a post might not all be directly attached to this post.
			 * We'll use the Ticket information to get the ID of the post the Ticket is attached to and
			 * then get the Global Stock for that post.
			 */
			$ticket_post_ids = array_reduce( $tickets, static function ( array $post_ids, Tribe__Tickets__Ticket_Object $ticket ) {
				$ticket_event_id = (int) $ticket->get_event_id();
				if ( ! in_array( $ticket_event_id, $post_ids, true ) ) {
					$post_ids[] = $ticket_event_id;
				}

				return $post_ids;
			}, [] );

			foreach ( $ticket_post_ids as $ticket_post_id ) {
				$global_stock                  = new Tribe__Tickets__Global_Stock( $ticket_post_id );
				$global_stock                  = $global_stock->is_enabled() ? $global_stock->get_stock_level() : 0;
				$types['tickets']['available'] += $global_stock;

				// If there's at least one ticket with shared capacity add the global stock to the stock total.
				if ( ! self::tickets_own_stock( $ticket_post_id ) ) {
					$types['tickets']['stock'] += $global_stock;
				}
			}

			/**
			 * Allow filtering of ticket counts by event.
			 *
			 * @since 5.5.10
			 *
			 * @param array $types   An array of ticket types.
			 * @param int   $post_id The event post ID.
			 */
			return apply_filters( 'tec_tickets_get_ticket_counts', $types, $post_id );
		}

		/**
		 * Process RSVP counts.
		 *
		 * @since 5.5.9
		 *
		 * @param Tribe__Tickets__Ticket_Object $rsvp RSVP ticket object.
		 * @param array                         $types Array of ticket types.
		 *
		 * @return array
		 */
		public static function process_rsvp_counts( $rsvp, $types ) {
			$types['rsvp']['count'] ++;

			$types['rsvp']['stock'] += $rsvp->stock;

			if ( 0 !== $types['rsvp']['stock'] ) {
				$types['rsvp']['available'] ++;
			}

			if ( ! $rsvp->manage_stock() ) {
				$types['rsvp']['unlimited'] ++;
				$types['rsvp']['available'] ++;
			}

			return $types;
		}

		/**
		 * Returns if the all the tickets for an event
		 * have own stock
		 *
		 * @param int $post_id ID of parent "event" post
		 * @return bool
		 */
		public static function tickets_own_stock( $post_id ) {
			$tickets = self::get_all_event_tickets( $post_id );

			// if no tickets or rsvp return false
			if ( ! $tickets ) {
				return false;
			}

			foreach ( $tickets as $ticket ) {

				// if ticket and not RSVP
				if ( 'Tribe__Tickets__RSVP' !== $ticket->provider_class ) {

					$global_stock_mode = $ticket->global_stock_mode();

					if ( Tribe__Tickets__Global_Stock::OWN_STOCK_MODE !== $global_stock_mode ) {
						return false;
					}
				}
			}

			return true;
		}

		/**
		 * Tries to make data about global stock levels and global stock-enabled ticket objects
		 * available to frontend scripts.
		 *
		 * @deprecated 4.11.3
		 *
		 * @param array $tickets
		 */
		public static function add_frontend_stock_data( array $tickets ) {

			_deprecated_function( __METHOD__, '4.11.3', 'tribe( "tickets.editor.blocks.tickets" )->assets()' );

			if ( is_admin() ) {
				return;
			}

			/*
			 * Add the frontend ticket form script as needed (we do this lazily since right now),
			 * it's only required for certain combinations of event/ticket.
			 */
			if ( ! empty( self::$frontend_script_enqueued ) ) {
				return;
			}

			$plugin = Tribe__Tickets__Main::instance();

			wp_register_script(
				'wp-util-not-in-footer',
				includes_url( '/js/wp-util.js' ),
				[ 'jquery', 'underscore' ],
				false,
				false
			);

			wp_enqueue_script( 'wp-util-not-in-footer' );

			// Check whether we use v1 or v2. We need to update this when we deprecate tickets v1.
			$tickets_js = tribe_tickets_new_views_is_enabled() ? 'v2/tickets-block.js' : 'tickets-block.js';

			tec_asset(
				$plugin,
				'tribe-tickets-block',
				$tickets_js,
				[
					'jquery',
					'tribe-common',
					'jquery-ui-datepicker',
					'wp-util-not-in-footer',
					'wp-i18n',
					'wp-hooks',
				],
				null,
				[
					'type'     => 'js',
					'groups'   => [ 'tribe-tickets-block-assets' ],
					'localize' => [
						[
							'name' => 'TribeTicketOptions',
							'data' => [ __CLASS__, 'get_asset_localize_data_for_ticket_options' ],
						],
						[
							'name' => 'TribeCurrency',
							'data' => [ __CLASS__, 'get_asset_localize_data_for_currencies' ],
						],
						[
							'name' => 'TribeCartEndpoint',
							'data' => [
								'url' => tribe_tickets_rest_url( '/cart/' ),
							],
						],
						[
							'name' => 'TribeMessages',
							'data' => self::set_messages(),
						],
						[
							'name' => 'TribeTicketsURLs',
							'data' => [ __CLASS__, 'get_asset_localize_data_for_cart_checkout_urls' ],
						],
					],
				]
			);

			tribe_asset_enqueue_group( 'tribe-tickets-block-assets' );

			self::$frontend_script_enqueued = true;
		}

		/**
		 * Takes any global stock data and makes it available via a wp_localize_script() call.
		 *
		 * @deprecated 4.11.0
		 */
		public static function enqueue_frontend_stock_data() {
			$data = [
				'tickets' => [],
				'events'  => [],
			];

			foreach ( self::$frontend_ticket_data as $ticket ) {
				$post = $ticket->get_event();

				if ( empty( $post ) ) {
					continue;
				}

				$post_id      = $post->ID;
				$global_stock = new Tribe__Tickets__Global_Stock( $post_id );
				$stock_mode   = $ticket->global_stock_mode();

				$ticket_data = [
					'event_id' => $post_id,
					'mode'     => $stock_mode,
					'cap'      => $ticket->capacity(),
				];

				if ( $ticket->managing_stock() ) {
					$ticket_data['stock'] = $ticket->available();
				}

				$data['events'][ $post_id ] = [
					'stock' => $global_stock->get_stock_level(),
				];

				$data['tickets'][ $ticket->ID ] = $ticket_data;
			}

			wp_localize_script( 'tribe-tickets-block', 'tribe_tickets_stock_data', $data );
		}

		/**
		 * Returns the array of active modules/providers.
		 *
		 * @static
		 *
		 * @return array $active_modules {
		 *      Ticket modules
		 *
		 *      @param mixed $module A class which extends this one, acts as a ticket provider.
		 * }
		 */
		public static function modules() {
			/**
			 * Filters the available tickets modules
			 *
			 * @param array $active_modules {
			 *      Ticket modules
			 *
			 *      @param mixed $module A class which extends this one, acts as a ticket provider.
			 * }
			 */
			return apply_filters( 'tribe_tickets_get_modules', self::$active_modules );
		}

		/**
		 * Returns the class name of the default module/provider.
		 *
		 * @since 4.6
		 * @since 5.19.1 Update default to Tickets Commerce.
		 *
		 * @return string
		 */
		public static function get_default_module() {
			$modules = array_keys( self::modules() );

			if ( 1 === count( $modules ) ) {
				// There's only one, just return it.
				Tribe__Tickets__Tickets::$default_module = array_shift( $modules );
			} else {
				// Remove RSVP and PayPal tickets for this part
				unset(
					$modules[ array_search( 'Tribe__Tickets__RSVP', $modules ) ]
				);

				if ( ! empty( $modules ) ) {
					// We just return the first, so we don't show favoritism
					$sliced = array_slice( $modules, 0, 1 );
					self::$default_module = reset( $sliced );
				} else {
					// Set Tickets Commerce as the default module.
					self::$default_module = 'TEC\\Tickets\\Commerce\\Module';
				}
			}

			/**
			 * Filters the default commerce module (provider)
			 *
			 * @since 4.6
			 *
			 * @param string default ticket module class name
			 * @param array array of ticket module class names
			 */
			return apply_filters( 'tribe_tickets_get_default_module', self::$default_module, $modules );
		}

		/**
		 * Get all the tickets for an event. Queries all active modules/providers.
		 *
		 * @static
		 *
		 * @param int $post_id ID of parent "event" post
		 *
		 * @return array
		 */
		final public static function get_event_tickets( $post_id ) {
			$tickets = [];

			foreach ( self::modules() as $class => $module ) {
				/** @var Tribe__Tickets__Tickets $obj */
				$obj = call_user_func( [ $class, 'get_instance' ] );

				$provider_tickets = $obj->get_tickets( $post_id );

				if ( ! empty( $provider_tickets ) && is_array( $provider_tickets ) ) {
					$tickets[] = $provider_tickets;
				}
			}

			return ! empty( $tickets ) ? call_user_func_array( 'array_merge', $tickets ) : [];
		}

		/**
		 * Generates and returns the email template for a group of attendees.
		 *
		 * @param array $tickets
		 * @return string
		 */
		public function generate_tickets_email_content( $tickets ) {
			return tribe_tickets_get_template_part( 'tickets/email', null, [ 'tickets' => $tickets ], false );
		}

		/**
		 * Send RSVPs/tickets email for attendees.
		 *
		 * @since 5.0.3
		 *
		 * @param array $attendees List of attendees.
		 * @param array $args      {
		 *      The list of arguments to use for sending ticket emails.
		 *
		 *      @type string       $subject     The email subject.
		 *      @type string       $content     The email content.
		 *      @type string       $from_name   The name to send tickets from.
		 *      @type string       $from_email  The email to send tickets from.
		 *      @type array|string $headers     The list of headers to send.
		 *      @type array        $attachments The list of attachments to send.
		 *      @type string       $provider    The provider slug (rsvp, tpp, woo, edd).
		 *      @type int          $post_id     The post/event ID to send the emails for.
		 *      @type string|int   $order_id    The order ID to send the emails for.
		 * }
		 *
		 * @return int The number of emails sent successfully.
		 */
		public function send_tickets_email_for_attendees( $attendees, $args = [] ) {
			$unique_attendees = [];

			// Collect the unique emails for attendees.
			foreach ( $attendees as $attendee ) {
				// If the attendee data is not provided, get it from the provider.
				if ( ! is_array( $attendee ) ) {
					$attendee = $this->get_attendee( $attendee );
				}

				// If invalid attendee is set, skip it.
				if ( ! $attendee ) {
					continue;
				}

				if ( ! isset( $unique_attendees[ $attendee['holder_email'] ] ) ) {
					$unique_attendees[ $attendee['holder_email'] ] = [];
				}

				$unique_attendees[ $attendee['holder_email'] ][] = $attendee;
			}

			$emails_sent = 0;

			// Handle purchaser emails.
			if ( ! empty( $args['send_purchaser_all'] ) ) {
				// Get the purchaser email from the first attendee.
				$first_attendee  = reset( $attendees );
				$purchaser_email = $first_attendee['purchaser_email'];

				// Make sure purchaser gets a list of all of the attendee tickets.
				$unique_attendees[ $purchaser_email ] = $attendees;
			}

			// Send an email with all RSVPs/tickets for each unique attendee.
			foreach ( $unique_attendees as $to => $tickets ) {
				$emails_sent += (int) $this->send_tickets_email_for_attendee( $to, $tickets, $args );
			}

			return 0 < $emails_sent;
		}

		/**
		 * Send RSVPs/tickets email for an attendee.
		 *
		 * @since 5.0.3
		 * @since 5.5.10 Adjusted the method to use the new Tickets Emails Handler.
		 * @since 5.6.0 Reverted the methods back to before 5.5.10, new Tickets Emails Handler via filters.
		 *
		 * @param string $to      The email to send the tickets to.
		 * @param array  $tickets The list of tickets to send.
		 * @param array  $args    {
		 *      The list of arguments to use for sending ticket emails.
		 *
		 *      @type string       $subject     The email subject.
		 *      @type string       $content     The email content.
		 *      @type string       $from_name   The name to send tickets from.
		 *      @type string       $from_email  The email to send tickets from.
		 *      @type array|string $headers     The list of headers to send.
		 *      @type array        $attachments The list of attachments to send.
		 *      @type string       $provider    The provider slug (rsvp, tpp, woo, edd).
		 *      @type int          $post_id     The post/event ID to send the emails for.
		 *      @type string|int   $order_id    The order ID to send the emails for.
		 * }
		 *
		 * @return bool Whether email was sent to attendees.
		 */
		public function send_tickets_email_for_attendee( $to, $tickets, $args = [] ) {
			/**
			 * Allows the short-circuiting of the sending of emails to the Attendees.
			 *
			 * @since 5.6.0
			 *
			 * @param null|mixed $pre     Determine if we should continue.
			 * @param string     $to      The email to send the tickets to.
			 * @param array      $tickets The list of tickets to send.
			 * @param array      $args    The list of arguments to use for sending ticket emails.
			 * @param static     $module  Instance of the Tickets Module.
			 */
			$pre = apply_filters( 'tec_tickets_send_tickets_email_for_attendee_pre', null, $to, $tickets, $args, $this );

			if ( null !== $pre ) {
				return $pre;
			}

			// If no tickets to send for, do not send email.
			if ( empty( $tickets ) ) {
				return false;
			}

			$defaults = [
				'subject'       => '',
				'content'       => '',
				'from_name'     => '',
				'from_email'    => '',
				'headers'       => [],
				'attachments'   => [],
				'provider'      => 'ticket',
				'post_id'       => 0,
				'order_id'      => '',
				'send_callback' => 'wp_mail',
			];

			// Set up the default arguments.
			$args = array_merge( $defaults, $args );

			$subject       = trim( (string) $args['subject'] );
			$content       = trim( (string) $args['content'] );
			$from_name     = trim( (string) $args['from_name'] );
			$from_email    = trim( (string) $args['from_email'] );
			$headers       = $args['headers'];
			$attachments   = $args['attachments'];
			$provider      = $args['provider'];
			$post_id       = $args['post_id'];
			$order_id      = $args['order_id'];
			$send_callback = $args['send_callback'];

			// If invalid send callback, do not send the email.
			if ( ! is_callable( $send_callback ) ) {
				return false;
			}

			// Set up default content.
			if ( empty( $content ) ) {
				$content = $this->generate_tickets_email_content( $tickets );
			}

			// Set up default subject.
			if ( empty( $subject ) ) {
				$site_name = stripslashes_deep( html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
				$is_rsvp   = 'rsvp' === $provider;

				$singular = $is_rsvp
					? tribe_get_rsvp_label_singular( 'RSVP email send' )
					: tribe_get_ticket_label_singular_lowercase( 'ticket email send' );

				$plural = $is_rsvp
					? tribe_get_rsvp_label_plural( 'RSVPs email send' )
					: tribe_get_ticket_label_plural_lowercase( 'tickets email send' );

				// translators: %1$s: The singular of "RSVP" or "ticket", %2$s: The plural of "RSVPs" or "tickets", %3$s: The site name.
				$subject_string = _nx( 'Your %1$s from %3$s', 'Your %2$s from %3$s', count( $tickets ), 'The default RSVP/ticket email subject', 'event-tickets' );

				$subject = sprintf(
					$subject_string,
					$singular,
					$plural,
					$site_name
				);
			}

			// Enforce headers array.
			if ( ! is_array( $headers ) ) {
				$headers = explode( "\r\n", $headers );
			}

			// Add From name/email to headers if no headers set yet and we have a valid From email address.
			if ( empty( $headers ) && ! empty( $from_name ) && ! empty( $from_email ) && is_email( $from_email ) ) {
				$from_email = filter_var( $from_email, FILTER_SANITIZE_EMAIL );

				$headers[] = sprintf(
					'From: %1$s <%2$s>',
					stripcslashes( $from_name ),
					$from_email
				);

				$headers[] = sprintf(
					'Reply-To: %s',
					$from_email
				);
			}

			// Enforce text/html content type header.
			if ( ! in_array( 'Content-type: text/html', $headers, true ) || ! in_array( 'Content-type: text/html; charset=utf-8', $headers, true ) ) {
				$headers[] = 'Content-type: text/html; charset=utf-8';
			}

			/**
			 * Allow filtering the email recipient for a provider. Backwards compatible with previous provider filter.
			 *
			 * The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
			 *
			 * @deprecated 5.0.3 Use the tribe_tickets_ticket_email_recipient filter instead.
			 *
			 * @since 4.7.6
			 *
			 * @since 5.0.3
			 *
			 * @param string     $to       The email to send to.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 */
			$to = apply_filters( "tribe_{$provider}_email_recipient", $to, $post_id, $order_id, $tickets );

			/**
			 * Allow filtering the email recipient.
			 *
			 * @since 5.0.3
			 *
			 * @param string     $to       The email to send to.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 * @param string     $provider The provider slug.
			 * @param array      $args     The full list of ticket email arguments as sent to the function.
			 */
			$to = apply_filters( 'tribe_tickets_ticket_email_recipient', $to, $post_id, $order_id, $tickets, $provider, $args );

			// If no email set or invalid email is used, do not send the email.
			if ( empty( $to ) || ! is_email( $to ) ) {
				return false;
			}

			/**
			 * Allow filtering the email subject for a provider. Backwards compatible with previous provider filter.
			 *
			 * The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
			 *
			 * @deprecated 5.0.3 Use the tribe_tickets_ticket_email_subject filter instead.
			 *
			 * @since 4.7.6
			 *
			 * @param string     $subject  The email subject.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 */
			$subject = apply_filters( "tribe_{$provider}_email_subject", $subject, $post_id, $order_id, $tickets );

			/**
			 * Allow filtering the email subject.
			 *
			 * @since 5.0.3
			 *
			 * @param string     $subject  The email subject.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 * @param string     $provider The provider slug.
			 * @param array      $args     The full list of ticket email arguments as sent to the function.
			 */
			$subject = apply_filters( 'tribe_tickets_ticket_email_subject', $subject, $post_id, $order_id, $tickets, $provider, $args );

			// If no subject to use for the email, do not send the email.
			if ( empty( $subject ) ) {
				return false;
			}

			// Generate the email content for the tickets.
			$content = $this->generate_tickets_email_content( $tickets );

			/**
			 * Allow filtering the email content for a provider. Backwards compatible with previous provider filter.
			 *
			 * The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
			 *
			 * @deprecated 5.0.3 Use the tribe_tickets_ticket_email_content filter instead.
			 *
			 * @since 4.7.6
			 *
			 * @param array      $content  The content to send the email with.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 */
			$content = apply_filters( "tribe_{$provider}_email_content", $content, $post_id, $order_id, $tickets );

			/**
			 * Allow filtering the email content.
			 *
			 * @since 5.0.3
			 *
			 * @param array      $content  The content to send the email with.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 * @param string     $provider The provider slug.
			 * @param array      $args     The full list of ticket email arguments as sent to the function.
			 */
			$content = apply_filters( 'tribe_tickets_ticket_email_content', $content, $post_id, $order_id, $tickets, $provider, $args );

			// If no content to use for the email, do not send the email.
			if ( empty( $content ) ) {
				return false;
			}

			/**
			 * Allow filtering the email headers for a provider. Backwards compatible with previous provider filter.
			 *
			 * The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
			 *
			 * @deprecated 5.0.3 Use the tribe_tickets_ticket_email_headers filter instead.
			 *
			 * @since 4.7.6
			 *
			 * @param array      $headers  List of email headers.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 */
			$headers = apply_filters( "tribe_{$provider}_email_headers", $headers, $post_id, $order_id, $tickets );

			/**
			 * Allow filtering the email headers.
			 *
			 * @since 5.0.3
			 *
			 * @param array      $headers  List of email headers.
			 * @param int        $post_id  The post/event ID to send the email for.
			 * @param string|int $order_id The order ID to send the email for.
			 * @param array      $tickets  The list of tickets to send.
			 * @param string     $provider The provider slug.
			 * @param array      $args     The full list of ticket email arguments as sent to the function.
			 */
			$headers = apply_filters( 'tribe_tickets_ticket_email_headers', $headers, $post_id, $order_id, $tickets, $provider, $args );

			/**
			 * Allow filtering the email attachments for a provider. Backwards compatible with previous provider filter.
			 *
			 * The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
			 *
			 * @deprecated 5.0.3 Use the tribe_tickets_ticket_email_attachments filter instead.
			 *
			 * @since 4.7.6
			 *
			 * @param array      $attachments The list of attachments to send.
			 * @param int        $post_id     The post/event ID to send the email for.
			 * @param string|int $order_id    The order ID to send the email for.
			 * @param array      $tickets     The list of tickets to send.
			 */
			$attachments = apply_filters( "tribe_{$provider}_email_attachments", $attachments, $post_id, $order_id, $tickets );

			/**
			 * Allow filtering the email attachments.
			 *
			 * @since 5.0.3
			 *
			 * @param array      $attachments The list of attachments to send.
			 * @param int        $post_id     The post/event ID to send the email for.
			 * @param string|int $order_id    The order ID to send the email for.
			 * @param array      $tickets     The list of tickets to send.
			 * @param string     $provider The provider slug.
			 * @param array      $args     The full list of ticket email arguments as sent to the function.
			 */
			$attachments = apply_filters( 'tribe_tickets_ticket_email_attachments', $attachments, $post_id, $order_id, $tickets, $provider, $args );

			$sent = $send_callback( $to, $subject, $content, $headers, $attachments );

			// Handle marking the attendee ticket email as being sent.
			if ( $sent ) {
				// Mark attendee ticket email as being sent for each attendee ticket.
				foreach ( $tickets as $attendee ) {
					$this->update_ticket_sent_counter( $attendee['attendee_id'] );

					$this->update_attendee_activity_log(
						$attendee['attendee_id'],
						[
							'type'  => 'email',
							'name'  => $attendee['holder_name'],
							'email' => $attendee['holder_email'],
						]
					);
				}
			}

			return $sent;
		}

		/**
		 * Update the email sent counter for attendee by increasing it +1.
		 *
		 * @since 5.1.0
		 *
		 * @param int $attendee_id The attendee ID.
		 */
		public function update_ticket_sent_counter( $attendee_id ) {
			$prev_val = (int) get_post_meta( $attendee_id, $this->attendee_ticket_sent, true );

			update_post_meta( $attendee_id, $this->attendee_ticket_sent, $prev_val + 1 );
		}

		/**
		 * Update the attendee activity log data.
		 *
		 * @param int   $attendee_id Attendee ID.
		 * @param array $data Data that needs to be logged.
		 *
		 * @since 5.1.0
		 */
		public function update_attendee_activity_log( $attendee_id, $data = [] ) {

			$activity = get_post_meta( $attendee_id, $this->attendee_activity_log, true );

			if ( ! is_array( $activity ) ) {
				$activity = [];
			}

			/**
			 * Filter the activity log data for attendee.
			 *
			 * @since 5.1.0
			 *
			 * @param array $data Activity data.
			 * @param int   $attendee_id Attendee ID.
			 */
			$data = apply_filters( 'tribe_tickets_attendee_activity_log_data', $data, $attendee_id );

			$data['time'] = time();

			$activity[] = $data;

			update_post_meta( $attendee_id, $this->attendee_activity_log, $activity );
		}

		/**
		 * Gets the view from the plugin's folder, or from the user's theme if found.
		 *
		 * @param string $template
		 * @return mixed|void
		 */
		public function getTemplateHierarchy( $template ) {

			if ( substr( $template, - 4 ) != '.php' ) {
				$template .= '.php';
			}

			if ( $theme_file = locate_template( [ 'tribe-events/' . $template ] ) ) {
				$file = $theme_file;
			} else {
				$file = $this->plugin_path . 'src/views/' . $template;
			}

			return apply_filters( 'tribe_events_tickets_template_' . $template, $file );
		}

		/**
		 * Formats the cost based on the provider of a ticket of an event.
		 *
		 * @param float|string $cost
		 * @param int   		$post_id
		 *
		 * @return string
		 */
		public function maybe_format_event_cost( $cost, $post_id ) {
			$tickets = self::get_all_event_tickets( $post_id );
			// If $cost isn't a number or there are no tickets, no filter needed.
			if ( ! is_numeric( $cost ) || empty( $tickets ) ) {
				return $cost;
			}
			$currency = tribe( 'tickets.commerce.currency' );
			// We will convert to the format of the first ticket's provider class.
			return $currency->get_formatted_currency( $cost, null, $tickets[0]->provider_class );
		}

		/**
		 * Queries ticketing providers to establish the range of tickets/pricepoints for the specified
		 * event and ensures those costs are included in the $costs array.
		 *
		 * @param array $prices
		 * @param int   $post_id
		 * @return array
		 */
		public function get_ticket_prices( array $prices, $post_id ) {
			// If value already exists, do not override it. Return it.
			if ( ! empty( $prices ) ) {
				return $prices;
			}

			// Iterate through all tickets from all providers
			foreach ( self::get_all_event_tickets( $post_id ) as $ticket ) {
				// No need to add the pricepoint if it is already in the array
				if ( in_array( $ticket->price, $prices ) ) {
					continue;
				}

				// An empty price property can be ignored (but do add if the price is explicitly set to zero).
				if ( isset( $ticket->price ) && is_numeric( $ticket->price ) ) {
					$prices[] = $ticket->price;
				}
			}

			return $prices;
		}

		/**
		 * Filter past tickets from showing up in cost range.
		 *
		 * @since 5.1.5
		 *
		 * @param array  $costs List of ticket costs.
		 * @param int    $post_id Target Event's ID.
		 * @param string $meta Meta key name.
		 * @param bool   $single determines if the requested meta should be a single item or an array of items.
		 *
		 * @return array The list of ticket costs with past tickets excluded possibly.
		 */
		public function exclude_past_tickets_from_cost_range( $costs, $post_id, $meta, $single ) {

			if ( '_EventCost' != $meta || $single || empty( $costs )  ) {
				return $costs;
			}

			/**
			 * Allow filtering of whether to exclude past tickets in the event cost range.
			 *
			 * @since 5.1.4
			 *
			 * @param bool  $exclude_past_tickets Whether to exclude past tickets in the event cost range.
			 * @param array $costs                Which costs are going to be displayed.
			 * @param int   $post_id              Which Event/Post we are dealign with.
			 */
			$exclude_past_tickets = apply_filters( 'event_tickets_exclude_past_tickets_from_cost_range', false, $costs, $post_id );

			if ( ! $exclude_past_tickets ) {
				return $costs;
			}

			$tickets = self::get_all_event_tickets( $post_id );

			$wp_timezone = Tribe__Timezones::wp_timezone_string();

			if ( Tribe__Timezones::is_utc_offset( $wp_timezone ) ) {
				$wp_timezone = Tribe__Timezones::generate_timezone_string_from_utc_offset( $wp_timezone );
			}

			$timezone = new DateTimeZone( $wp_timezone );

			foreach ( $tickets as $ticket ) {

				$now        = Tribe__Date_Utils::build_date_object( 'now', $timezone );
				$start_date = Tribe__Date_Utils::build_date_object( $ticket->start_date . ' ' . $ticket->start_time, $timezone );
				$end_date   = Tribe__Date_Utils::build_date_object( $ticket->end_date . ' ' . $ticket->end_time, $timezone );

				// If the ticket has not yet become available for sale or has already ended.
				if ( $now < $start_date || $end_date < $now ) {
					// Try to find the ticket price in the list of costs.
					$key = array_search( $ticket->price, $costs );

					// Remove the value from the list of costs if we found it.
					if ( false !== $key ) {
						unset( $costs[ $key ] );
					}
					continue;
				}
			}

			return $costs;
		}

		/**
		 * Given a valid attendee ID, returns the event ID it relates to or else boolean false
		 * if it cannot be determined.
		 *
		 * @param int   $attendee_id
		 * @return mixed int|bool
		 */
		public function get_event_id_from_attendee_id( $attendee_id ) {
			$provider_class     = new ReflectionClass( $this );
			$attendee_event_key = $this->get_attendee_event_key( $provider_class );

			if ( empty( $attendee_event_key ) ) {
				return false;
			}

			$post_id = get_post_meta( $attendee_id, $attendee_event_key, true );

			if ( empty( $post_id ) ) {
				return false;
			}

			return (int) $post_id;
		}

		/**
		 * Given a valid order ID, returns a single event ID it relates to or else boolean false
		 * if it cannot be determined.
		 *
		 * @see Use tribe_tickets_get_event_ids() to return an array of all event ids for an order
		 *
		 * @param int   $order_id
		 * @return mixed int|bool
		 */
		public function get_event_id_from_order_id( $order_id ) {
			$provider_class     = new ReflectionClass( $this );
			$attendee_order_key = $this->get_attendee_order_key( $provider_class );
			$attendee_event_key = $this->get_attendee_event_key( $provider_class );
			$attendee_object    = $this->get_attendee_object( $provider_class );

			if ( empty( $attendee_order_key ) || empty( $attendee_event_key ) || empty( $attendee_object ) ) {
				return false;
			}

			$first_matched_attendee = get_posts( [
				'post_type'  => $attendee_object,
				'meta_key'   => $attendee_order_key,
				'meta_value' => $order_id,
				'posts_per_page' => 1,
			] );

			if ( empty( $first_matched_attendee ) ) {
				return false;
			}

			return $this->get_event_id_from_attendee_id( $first_matched_attendee[0]->ID );
		}

		/**
		 * Returns the meta key used to link attendees with orders.
		 *
		 * This method provides backwards compatibility with older ticketing providers
		 * that do not define the expected class constants. Once a decent period has
		 * elapsed we can kill this method and access the class constants directly.
		 *
		 * @param ReflectionClass $provider_class representing the concrete ticket provider
		 * @return string
		 */
		protected function get_attendee_order_key( $provider_class ) {
			$attendee_order_key = $provider_class->getConstant( 'ATTENDEE_ORDER_KEY' );

			if ( ! empty( $attendee_order_key ) ) {
				return (string) $attendee_order_key;
			}

			switch ( $this->class_name ) {
				case 'Tribe__Events__Tickets__Woo__Main':
					return '_tribe_wooticket_order';
				case 'Tribe__Events__Tickets__EDD__Main':
					return '_tribe_eddticket_order';
				case 'Tribe__Events__Tickets__Shopp__Main':
					return '_tribe_shoppticket_order';
				case 'Tribe__Events__Tickets__Wpec__Main':
					return '_tribe_wpecticket_order';
				default:
					return '';
			}
		}

		/**
		 * Returns the attendee object post type.
		 *
		 * This method provides backwards compatibility with older ticketing providers
		 * that do not define the expected class constants. Once a decent period has
		 * elapsed we can kill this method and access the class constants directly.
		 *
		 * @param ReflectionClass $provider_class representing the concrete ticket provider
		 * @return string
		 */
		protected function get_attendee_object( $provider_class ) {
			$attendee_object = $provider_class->getConstant( 'ATTENDEE_OBJECT' );

			if ( ! empty( $attendee_object ) ) {
				return (string) $attendee_object;
			}

			switch ( $this->class_name ) {
				case 'Tribe__Events__Tickets__Woo__Main':
					return 'tribe_wooticket';
				case 'Tribe__Events__Tickets__EDD__Main':
					return 'tribe_eddticket';
				case 'Tribe__Events__Tickets__Shopp__Main':
					return 'tribe_shoppticket';
				case 'Tribe__Events__Tickets__Wpec__Main':
					return 'tribe_wpecticket';
				default:
					return '';
			}
		}


		/**
		 * Given a ticket provider, get its Attendee Optout Meta Key from its class property (or constant if legacy).
		 *
		 * @since 4.12.3
		 *
		 * @param self|string $provider Examples: 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main', 'woo', 'rsvp', etc.
		 *
		 * @return string The meta key or an empty string if passed an invalid or inactive ticket provider.
		 */
		public static function get_attendee_optout_key( $provider ) {
			$provider = static::get_ticket_provider_instance( $provider );

			if ( empty( $provider ) ) {
				return '';
			}

			/**
			 * Not all classes have this static method.
			 *
			 * @see \Tribe__Tickets__Commerce__PayPal__Main::get_key() Does have this static method.
			 */
			if ( method_exists( $provider, 'get_key' ) ) {
				$key = $provider::get_key( 'attendee_optout_key' );
			}

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

			if ( ! empty( $provider->attendee_optout_key ) ) {
				return $provider->attendee_optout_key;
			}

			$key = constant( "{$provider->class_name}::ATTENDEE_OPTOUT_KEY" );

			return (string) $key;
		}

		/**
		 * Returns the meta key used to link attendees with the base event.
		 *
		 * This method provides backwards compatibility with older ticketing providers
		 * that do not define the expected class constants. Once a decent period has
		 * elapsed we can kill this method and access the class constants directly.
		 *
		 * If the meta key cannot be determined the returned string will be empty.
		 *
		 * @param ReflectionClass $provider_class representing the concrete ticket provider
		 * @return string
		 */
		protected function get_attendee_event_key( $provider_class ) {
			$attendee_event_key = $provider_class->getConstant( 'ATTENDEE_EVENT_KEY' );

			if ( ! empty( $attendee_event_key ) ) {
				return (string) $attendee_event_key;
			}

			switch ( $this->class_name ) {
				case 'Tribe__Events__Tickets__Woo__Main':
					return '_tribe_wooticket_event';
				case 'Tribe__Events__Tickets__EDD__Main':
					return '_tribe_eddticket_event';
				case 'Tribe__Events__Tickets__Shopp__Main':
					return '_tribe_shoppticket_event';
				case 'Tribe__Events__Tickets__Wpec__Main':
					return '_tribe_wpecticket_event';
				default:
					return '';
			}
		}

		/**
		 * Process the attendee meta into an array with value, slug, and label
		 *
		 * @param int $product_id
		 * @param array $meta
		 * @return array
		 */
		public function process_attendee_meta( $product_id, $meta ) {
			$meta_values = [];

			if ( ! class_exists( 'Tribe__Tickets_Plus__Main' ) ) {
				return $meta_values;
			}

			$meta_field_objects = Tribe__Tickets_Plus__Main::instance()->meta()->get_meta_fields_by_ticket( $product_id );

			foreach ( $meta_field_objects as $field ) {
				$value = null;

				if ( 'checkbox' === $field->type ) {
					$field_prefix = $field->slug . '_';
					$value        = [];

					foreach ( $meta as $full_key => $check_value ) {
						if ( 0 === strpos( $full_key, $field_prefix ) ) {
							$short_key           = substr( $full_key, strlen( $field_prefix ) );
							$value[ $short_key ] = $check_value;
						}
					}

					if ( empty( $value ) ) {
						$value = null;
					}
				} elseif ( isset( $meta[ $field->slug ] ) ) {
					$value = $meta[ $field->slug ];
				}

				$meta_values[ $field->slug ] = [
					'slug'  => $field->slug,
					'label' => $field->label,
					'value' => $value,
				];
			}

			return $meta_values;
		}

		/**
		 * Returns the meta key used to link ticket types with the base event.
		 *
		 * Subclasses can override this if they use a key other than 'event_key'
		 * for this purpose.
		 *
		 * @since 5.14.0 Removed check for static property. All static properties were removed over 2 major versions ago.
		 *
		 * @return string
		 */
		public function get_event_key() {
			return $this->event_key;
		}

		/**
		 * Returns an availability slug based on all tickets in the provided collection
		 *
		 * The availability slug is used for CSS class names and filter helper strings
		 *
		 * @since 4.2
		 *
		 * @param array $tickets Collection of tickets
		 * @param string $datetime Datetime string
		 * @return string
		 */
		public function get_availability_slug_by_collection( $tickets, $datetime = null ) {
			if ( ! $tickets ) {
				return;
			}

			$collection_availability_slug = 'available';
			$tickets_available = false;
			$slugs = [];

			/** @var Tribe__Tickets__Ticket_Object $ticket */

			foreach ( $tickets as $ticket ) {
				$availability_slug = $ticket->availability_slug( $datetime );

				// if any ticket is available for this event, consider the availability slug as 'available'
				if ( 'available' === $availability_slug ) {
					// reset the collected slugs to "available" only
					$slugs = [ 'available' ];
					break;
				}

				// track unique availability slugs
				if ( ! in_array( $availability_slug, $slugs, true ) ) {
					$slugs[] = $availability_slug;
				}
			}

			if ( 1 === count( $slugs ) ) {
				$collection_availability_slug = $slugs[0];
			} else {
				$collection_availability_slug = 'availability-mixed';
			}

			/**
			 * Filters the availability slug for a collection of tickets
			 *
			 * @param string Availability slug
			 * @param array Collection of tickets
			 * @param string Datetime string
			 */
			return apply_filters( 'event_tickets_availability_slug_by_collection', $collection_availability_slug, $tickets, $datetime );
		}

		/**
		 * Returns a tickets unavailable message based on the availability slug of a collection of tickets
		 *
		 * @since 4.2
		 * @since 4.10.9 Use customizable ticket name functions.
		 *
		 * @param array $tickets Collection of tickets
		 * @return string
		 */
		public function get_tickets_unavailable_message( $tickets ) {
			$availability_slug = $this->get_availability_slug_by_collection( $tickets );
			$message           = null;
			$post_type = get_post_type();

			if (
				'tribe_events' == $post_type
				&& function_exists( 'tribe_is_past_event' )
				&& tribe_is_past_event()
			) {
				$events_label_singular_lowercase = tribe_get_event_label_singular_lowercase();
				$message = esc_html( sprintf( __( '%s are not available as this %s has passed.', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_past_tribe_events' ), $events_label_singular_lowercase ) );
			} elseif ( 'availability-future' === $availability_slug ) {
				/**
				 * Allows inclusion of ticket start sale date in unavailability message
				 *
				 * @since 4.7.6
				 *
				 * @param bool	$display_date
				 */
				$display_date = apply_filters( 'tribe_tickets_unvailable_message_date', $display_date = true );

				/**
				 * Allows inclusion of ticket start sale time in unavailability message
				 *
				 * @since 4.7.6
				 *
				 * @param bool	$display_time
				 */
				$display_time = apply_filters( 'tribe_tickets_unvailable_message_time', $display_time = false );

				// build message
				if ( $display_date ) {
					$start_sale_date = '';
					$start_sale_time = '';

					foreach ( $tickets as $ticket ) {
						// get the earliest start sale date
						if ( '' == $start_sale_date || $ticket->start_date < $start_sale_date ) {
							$start_sale_date = $ticket->start_date;
							$start_sale_time = $ticket->start_time;
						}
					}

					$date_format = tribe_get_date_format( true );
					$start_sale_date = Tribe__Date_Utils::build_date_object( $start_sale_date )->format_i18n( $date_format );

					$message = esc_html( sprintf( __( '%s will be available on ', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_future_display_date' ) ) );
					$message .= $start_sale_date;

					if ( $display_time ) {
						$time_format = tribe_get_time_format();
						$start_sale_time = Tribe__Date_Utils::build_date_object( $start_sale_time )->format_i18n( $time_format );
						$message .= __( ' at ', 'event_tickets' ) . $start_sale_time;
					}
				} else {
					$message = esc_html( sprintf( __( '%s are not yet available', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_future_without_date' ) ) );
				}
			} elseif ( 'availability-past' === $availability_slug ) {
				$message = esc_html( sprintf( __( '%s are no longer available.', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_past' ) ) );
			} elseif ( 'availability-mixed' === $availability_slug ) {
				$message = esc_html( sprintf( __( 'There are no %s available at this time.', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_mixed' ) ) );
			}

			/**
			 * Filters the unavailability message for a ticket collection
			 *
			 * @param string Unavailability message
			 * @param array Collection of tickets
			 */
			$message = apply_filters( 'event_tickets_unvailable_message', $message, $tickets );

			return $message;
		}

		/**
		 * Indicates that, from an individual ticket provider's perspective, the only tickets for the
		 * event are currently unavailable and unless a different ticket provider reports differently
		 * the "tickets unavailable" message should be displayed.
		 *
		 * @param array $tickets
		 * @param int $post_id ID of parent "event" post (defaults to the current post)
		 */
		public function maybe_show_tickets_unavailable_message( $tickets, $post_id = null ) {
			if ( null === $post_id ) {
				$post_id = get_the_ID();
			}

			$unavailable_tickets = self::$currently_unavailable_tickets;

			$existing_tickets = ! empty( $unavailable_tickets[ (int) $post_id ] )
				? $unavailable_tickets[ (int) $post_id ]
				: [];

			self::$currently_unavailable_tickets[ (int) $post_id ] = array_merge( $existing_tickets, $tickets );


		}

		/**
		 * Indicates that, from an individual ticket provider's perspective, the event does have some
		 * currently available tickets and so the "tickets unavailable" message should probably not
		 * be displayed.
		 *
		 * @param null $post_id
		 */
		public function do_not_show_tickets_unavailable_message( $post_id = null ) {
			if ( null === $post_id ) {
				$post_id = get_the_ID();
			}

			self::$posts_with_available_tickets[] = (int) $post_id;
		}

		/**
		 * If appropriate, display a "tickets unavailable" message.
		 */
		public function show_tickets_unavailable_message() {
			$post_id = (int) get_the_ID();

			// So long as at least one ticket provider has tickets available, do not show an unavailability message
			if ( in_array( $post_id, self::$posts_with_available_tickets, true ) ) {
				return;
			}

			// Bail if no ticket providers reported that all their tickets for the event were unavailable
			if ( empty( self::$currently_unavailable_tickets[ $post_id ] ) ) {
				return;
			}

			// Prepare the message
			$message = '<div class="tickets-unavailable">'
				. $this->get_tickets_unavailable_message( self::$currently_unavailable_tickets[ $post_id ] )
				. '</div>';

			/**
			 * Sets the tickets unavailable message.
			 *
			 * @param string $message
			 * @param int    $post_id
			 * @param array  $unavailable_event_tickets
			 */
			echo apply_filters( 'tribe_tickets_unavailable_message', $message, $post_id, self::$currently_unavailable_tickets[ $post_id ] );

			// Remove the record of unavailable tickets to avoid duplicate messages being rendered for the same event
			unset( self::$currently_unavailable_tickets[ $post_id ] );
		}

		/**
		 * Takes care of adding a "tickets unavailable" message by injecting it into the post content
		 * (where the template settings require such an approach).
		 *
		 * @param string $content
		 * @return string
		 */
		public function show_tickets_unavailable_message_in_content( $content ) {
			if ( ! $this->should_inject_ticket_form_into_post_content() ) {
				return $content;
			}

			ob_start();
			$this->show_tickets_unavailable_message();
			$form = ob_get_clean();

			$content .= $form;

			return $content;
		}
		// end Helpers

		/**
		 * Associates an attendee record with a user, typically the purchaser.
		 *
		 * The $user_id param is optional and when not provided it will default to the current
		 * user ID.
		 *
		 *
		 * @param int $attendee_id
		 * @param int $user_id
		 */
		protected function record_attendee_user_id( $attendee_id, $user_id = null ) {
			if ( null === $user_id ) {
				$user_id = get_current_user_id();
			}

			update_post_meta( $attendee_id, $this->attendee_user_id, (int) $user_id );
		}

		/**
		 * Prints the front-end tickets form in the post content.
		 *
		 * @param string $content The post original content.
		 *
		 * @return string The updated content.
		 */
		public function front_end_tickets_form_in_content( $content ) {
			if ( ! $this->should_inject_ticket_form_into_post_content() ) {
				return $content;
			}

			ob_start();
			$this->front_end_tickets_form( $content );
			$form    = ob_get_clean();
			$content .= $form;

			return $content;
		}

		/**
		 * Determines if this is a suitable opportunity to inject ticket form content into a post.
		 * Expects to run within "the_content".
		 *
		 * @since 5.0.1 Bail if $post->ID is zero, such as from BuddyPress' "Activity" page.
		 *
		 * @return bool
		 */
		protected function should_inject_ticket_form_into_post_content() {
			global $post;

			// Prevents firing more then it needs to outside of the loop.
			$in_the_loop = isset( $GLOBALS['wp_query']->in_the_loop ) && $GLOBALS['wp_query']->in_the_loop;

			if (
				is_admin()
				|| ! $in_the_loop
			) {
				return false;
			}

			if ( ! is_singular() ) {
				return false;
			}

			// Bail if this isn't a post for some reason.
			// Empty check is for BuddyPress having a WP Post with ID of zero.
			if (
				! $post instanceof WP_Post
				|| empty( $post->ID )
			) {
				return false;
			}

			// Bail if this isn't a supported post type.
			if ( ! tribe_tickets_post_type_enabled( $post->post_type ) ) {
				return false;
			}

			// User is currently viewing/editing their existing tickets.
			if ( Tribe__Tickets__Tickets_View::instance()->is_edit_page() ) {
				return false;
			}

			// Bail if a tribe_events post because those post types are handled with a different hook.
			if (
				class_exists( 'Tribe__Events__Main' )
				&& defined( 'Tribe__Events__Main::POSTTYPE' )
				&& Tribe__Events__Main::POSTTYPE === $post->post_type
			) {
				return false;
			}

			// Bail if there aren't any tickets.
			$tickets = $this->get_tickets( $post->ID );
			if ( empty( $tickets ) ) {
				return false;
			}

			/** @var Tribe__Editor $editor */
			$editor = tribe( 'editor' );

			// Blocks and ticket templates merged - bail if we should be seeing blocks.
			if ( has_blocks( $post->ID ) ) {
				return false;
			}

			return true;
		}

		/**
		 * Indicates if the user must be logged in in order to obtain tickets.
		 *
		 * @since 4.7
		 *
		 * @return bool
		 */
		public function login_required() {
			$requirements = (array) tribe_get_option( 'ticket-authentication-requirements', [] );

			return in_array( 'event-tickets_all', $requirements, true );
		}

		/**
		 * Provides a URL that can be used to direct users to the login form.
		 *
		 * @param int $post_id - the ID of the post to redirect to
		 *
		 * @return string
		 */
		public static function get_login_url( $post_id = null ) {
			if ( is_null( $post_id ) ) {
				$post_id   = get_the_ID();
			}

			$login_url = get_site_url( null, 'wp-login.php' );

			if ( $post_id ) {
				$login_url = add_query_arg( 'redirect_to', get_permalink( $post_id ), $login_url );
			}

			/**
			 * Provides an opportunity to modify the login URL used within frontend
			 * ticket forms (typically when they need to login before they can proceed).
			 *
			 * @param string $login_url
			 */
			return apply_filters( 'tribe_tickets_ticket_login_url', $login_url );
		}

		/**
		 * Adds or updates the capacity for a ticket.
		 *
		 * @since 4.7
		 *
		 * @param WP_Post|int $ticket
		 * @param array       $raw_data
		 * @param string      $save_type
		 */
		public function update_capacity( $ticket, $data, $save_type ) {
			if ( empty( $data ) ) {
				return;
			}

			// set the default capacity to that of the event, if set, or to unlimited
			$default_capacity = (int) Tribe__Utils__Array::get( $data, 'event_capacity', -1 );

			// Fetch capacity field, if we don't have it use default (defined above)
			$data['capacity'] = trim( Tribe__Utils__Array::get( $data, 'capacity', $default_capacity ) );

			// If empty we need to modify to the default
			if ( '' === $data['capacity'] ) {
				$data['capacity'] = $default_capacity;
			}

			// The only available value lower than zero is -1 which is unlimited
			if ( 0 > $data['capacity'] ) {
				$data['capacity'] = -1;
			}

			// Fetch the stock if defined, otherwise use Capacity field
			$data['stock'] = trim( Tribe__Utils__Array::get( $data, 'stock', $data['capacity'] ) );

			// If empty we need to modify to what every capacity was
			if ( '' === $data['stock'] ) {
				$data['stock'] = $data['capacity'];
			}

			// The only available value lower than zero is -1 which is unlimited
			if ( 0 > $data['stock'] ) {
				$data['stock'] = -1;
			}

			if ( -1 !== $data['capacity'] ) {
				if ( 'update' === $save_type ) {
					/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
					$tickets_handler = tribe( 'tickets.handler' );

					$totals = $tickets_handler->get_ticket_totals( $ticket->ID );

					$data['stock'] -= $totals['pending'] + $totals['sold'];
				}

				update_post_meta( $ticket->ID, '_manage_stock', 'yes' );
				update_post_meta( $ticket->ID, '_stock', $data['stock'] );
			} else {
				// unlimited stock
				delete_post_meta( $ticket->ID, '_stock_status' );
				update_post_meta( $ticket->ID, '_manage_stock', 'no' );
				delete_post_meta( $ticket->ID, '_stock' );
				delete_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE );
				delete_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_CAP );
			}

			tribe_tickets_update_capacity( $ticket, $data['capacity'] );
		}

		/**
		 * @param bool $operation_did_complete
		 */
		protected function maybe_update_attendees_cache( $operation_did_complete ) {
			if ( $operation_did_complete && ! empty( $_POST['event_ID'] ) ) {
				$this->clear_attendees_cache( $_POST['event_ID'] );
			}
		}

		/**
		 * Clears the attendees cache for a given post
		 *
		 * @param int|WP_Post $post_id The parent post or ID
		 *
		 * @return bool Was the operation successful?
		 */
		public function clear_attendees_cache( $post_id ) {
			if ( $post_id instanceof WP_Post ) {
				$post_id = $post_id->ID;
			}

			/** @var Tribe__Post_Transient $post_transient */
			$post_transient = tribe( 'post-transient' );

			$cache_key = (int) $post_id;

			return $post_transient->delete( $cache_key, self::ATTENDEES_CACHE );
		}

		/**
		 * Clears the ticket cache for a given ticket ID.
		 *
		 * @since 5.1.0
		 *
		 * @param int|object $ticket_id The ticket ID.
		 */
		public function clear_ticket_cache( $ticket_id ) {
			if ( is_object( $ticket_id ) ) {
				$ticket_id = $ticket_id->ID;
			}

			$methods = [
				'Tribe__Tickets__Ticket_Object::is_in_stock',
				'Tribe__Tickets__Ticket_Object::inventory',
				'Tribe__Tickets__Ticket_Object::available',
				'Tribe__Tickets__Ticket_Object::capacity',
			];

			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );

			foreach ( $methods as $method ) {
				$key = $method . '-' . $ticket_id;

				unset( $cache[ $key ] );
			}
		}

		/**
		 * Returns the action tag that should be used to print the front-end ticket form.
		 *
		 * This value is set in the Events > Settings > Tickets tab and is distinct between RSVP
		 * tickets and commerce provided tickets.
		 *
		 * @return string
		 */
		public function get_ticket_form_hook() {
			if ( $this instanceof Tribe__Tickets__RSVP ) {
				$ticket_form_hook = Tribe__Settings_Manager::get_option( 'ticket-rsvp-form-location',
					'tribe_events_single_event_after_the_meta' );

				/**
				 * Filters the position of the RSVP tickets form.
				 *
				 * While this setting can be handled using the Events > Settings > Tickets > "Location of RSVP form"
				 * setting this filter allows developers to override the general setting in particular cases.
				 * Returning an empty value here will prevent the ticket form from printing on the page.
				 *
				 * @param string                  $ticket_form_hook The set action tag to print front-end RSVP tickets form.
				 * @param Tribe__Tickets__Tickets $tickets_obj      The current instance of the class that's hooking its front-end ticket form.
				 */
				$ticket_form_hook = apply_filters( 'tribe_tickets_rsvp_tickets_form_hook', $ticket_form_hook, $this );
			} else {
				$ticket_form_hook = Tribe__Settings_Manager::get_option( 'ticket-commerce-form-location',
					'tribe_events_single_event_after_the_meta' );

				/**
				 * Filters the position of the commerce-provided tickets form.
				 *
				 * While this setting can be handled using the Events > Settings > Tickets > "Location of Tickets form"
				 * setting this filter allows developers to override the general setting in particular cases.
				 * Returning an empty value here will prevent the ticket form from printing on the page.
				 *
				 * @param string                  $ticket_form_hook The set action tag to print front-end commerce tickets form.
				 * @param Tribe__Tickets__Tickets $tickets_obj      The current instance of the class that's hooking its front-end ticket form.
				 */
				$ticket_form_hook = apply_filters( 'tribe_tickets_commerce_tickets_form_hook', $ticket_form_hook, $this );
			}

			return $ticket_form_hook;
		}

		/**
		 * Creates a duplicate ticket based on post id and ticket id.
		 *
		 * @since 5.2.3
		 *
		 * @param int $post_id   ID of parent "event" post.
		 * @param int $ticket_id ID of ticket to duplicate.
		 *
		 * @return int|boolean $duplicate_ticket_id New ticket ID or false, if unable to create duplicate.
		 */
		public function duplicate_ticket( $post_id, $ticket_id ) {

			// Get ticket data.
			$ticket = $this->get_ticket( $post_id, $ticket_id );

			if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
				return false;
			}

			// Create data for duplicate ticket.
			$data = [
				'ticket_name'             => $ticket->name . __( '(copy)', 'event-tickets' ),
				'ticket_description'      => $ticket->description,
				'ticket_price'            => $ticket->price,
				'ticket_show_description' => $ticket->show_description,
				'ticket_start_date'       => $ticket->start_date,
				'ticket_start_time'       => $ticket->start_time,
				'ticket_end_date'         => $ticket->end_date,
				'ticket_end_time'         => $ticket->end_time,
				'tribe-ticket'            => [
					'capacity' => $ticket->capacity(),
					'mode'     => $ticket->global_stock_mode(),
				]
			];

			// Add the ticket.
			$duplicate_ticket_id = $this->ticket_add( $post_id, $data );

			if ( ! $duplicate_ticket_id ) {
				return false;
			}

			// Copy ticket meta from old ticket to new ticket.
			$ignore_meta = [
				'_sku',
				'_tribe_ticket_manual_updated',
				'_wp_old_slug',
				'total_sales',
			];
			$ticket_meta = get_post_meta( $ticket->ID );

			if ( $ticket_meta ) {
				foreach ( $ticket_meta as $meta_key => $meta_values ) {
					// Skip meta we don't want to duplicate.
					if ( false !== strpos( $meta_key, '_tec_tc_ticket_status_count' ) ){
						continue;
					}
					if ( in_array( $meta_key, $ignore_meta ) ) {
						continue;
					}

					// Delete duplicate tickets meta before adding new meta.
					delete_post_meta( $duplicate_ticket_id, $meta_key );

					foreach ( $meta_values as $meta_value ) {
						// Maybe convert to object, in case meta is serialized.
						$meta_value_obj = maybe_unserialize( $meta_value );
						add_post_meta( $duplicate_ticket_id, $meta_key, $meta_value_obj );
					}
				}
			}

			// Update SKU of new ticket to remove '(COPY)'.
			$old_sku = get_post_meta( $duplicate_ticket_id, '_sku', true );
			$new_sku = str_replace( '(COPY)', '', $old_sku );
			update_post_meta( $duplicate_ticket_id, '_sku', $new_sku, $old_sku );

			return $duplicate_ticket_id;
		}

		/**
		 * Clones a ticket to a new post.
		 *
		 * @since 5.6.3
		 *
		 * @param int $original_post_id ID of the original "event" post.
		 * @param int $new_post_id      ID of the new "event" post.
		 * @param int $ticket_id        ID of ticket to duplicate.
		 *
		 * @return int|boolean $duplicate_ticket_id New ticket ID or false, if unable to create duplicate.
		 */
		public function clone_ticket_to_new_post( $original_post_id, $new_post_id, $ticket_id ) {
			// Get ticket data.
			$ticket = $this->get_ticket( $original_post_id, $ticket_id );

			if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
				return false;
			}

			// Create data for duplicate ticket.
			$data = [
				'ticket_name'             => $ticket->name,
				'ticket_description'      => $ticket->description,
				'ticket_price'            => $ticket->price,
				'ticket_show_description' => $ticket->show_description,
				'ticket_start_date'       => $ticket->start_date,
				'ticket_start_time'       => $ticket->start_time,
				'ticket_end_date'         => $ticket->end_date,
				'ticket_end_time'         => $ticket->end_time,
				'tribe-ticket'            => [
					'capacity' => $ticket->capacity(),
					'mode'     => $ticket->global_stock_mode(),
				]
			];

			// Add the ticket.
			$duplicate_ticket_id = $this->ticket_add( $new_post_id, $data );

			if ( ! $duplicate_ticket_id ) {
				return false;
			}

			return $duplicate_ticket_id;
		}


		/**
		 * Creates a ticket object and calls the child save_ticket function
		 *
		 * @param int $post_id ID of parent "event" post
		 * @param array $data Raw post data
		 *
		 * @return boolean
		 */
		public function ticket_add( $post_id, $data ) {
			$ticket                   = new Tribe__Tickets__Ticket_Object();
			$ticket->ID               = isset( $data['ticket_id'] ) ? absint( $data['ticket_id'] ) : null;
			$update                   = ! empty( $ticket->ID );
			$ticket->name             = isset( $data['ticket_name'] ) ? esc_html( $data['ticket_name'] ) : null;
			$ticket->description      = isset( $data['ticket_description'] ) ? wp_kses_post( $data['ticket_description'] ) : '';
			$ticket->price            = ! empty( $data['ticket_price'] ) ? filter_var( trim( $data['ticket_price'] ), FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) : 0;
			$ticket->show_description = isset( $data['ticket_show_description'] ) ? 'yes' : 'no';
			$ticket->provider_class   = $this->class_name;
			$ticket->start_date       = null;
			$ticket->end_date         = null;
			$ticket->menu_order       = isset( $data['ticket_menu_order'] ) ? intval( $data['ticket_menu_order'] ) : null;

			/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
			$tickets_handler = tribe( 'tickets.handler' );

			$tickets_handler->toggle_manual_update_flag( true );

			if ( ! empty( $ticket->price ) ) {
				// remove non-money characters
				$ticket->price = preg_replace( '/[^0-9\.\,]/Uis', '', $ticket->price );
			}

			if ( ! empty( $data['ticket_start_date'] ) ) {
				$start_datetime = Tribe__Date_Utils::maybe_format_from_datepicker( $data['ticket_start_date'] );

				if ( ! empty( $data['ticket_start_time'] ) ) {
					$start_datetime .= ' ' . $data['ticket_start_time'];
					$ticket->start_time = date( Tribe__Date_Utils::DBTIMEFORMAT, strtotime( ( $start_datetime ) ) );
				}

				$ticket->start_date = date( Tribe__Date_Utils::DBDATEFORMAT, strtotime( $start_datetime ) );
			}

			if ( ! empty( $data['ticket_end_date'] ) ) {
				$end_datetime = Tribe__Date_Utils::maybe_format_from_datepicker( $data['ticket_end_date'] );

				if ( ! empty( $data['ticket_end_time'] ) ) {
					$end_datetime .= ' ' . $data['ticket_end_time'];
					$ticket->end_time = date( Tribe__Date_Utils::DBTIMEFORMAT, strtotime( ( $end_datetime ) ) );
				}

				$ticket->end_date = date( Tribe__Date_Utils::DBDATEFORMAT, strtotime( $end_datetime ) );
			}

			if ( $update ) {
				update_post_meta( $ticket->ID, '_type', $data['ticket_type'] ?? 'default' );
			}

			// Pass the control to the child object.
			$save_ticket = $this->save_ticket( $post_id, $ticket, $data );

			// Set the ticket type before the module saves the ticket.
			$ticket_type = 'default';
			if ( $update ) {
				$ticket_type = get_post_meta( $ticket->ID, '_type', true ) ?: 'default';
			}
			$new_ticket_type = ! empty( $data['ticket_type'] ) ? $data['ticket_type'] : $ticket_type;
			update_post_meta( $ticket->ID, '_type', $new_ticket_type );

			/**
			 * Fired once a ticket has been created and added to a post.
			 *
			 * @since 5.8.0 Add the `$update` parameter.
			 *
			 * @param int                           $post_id  The ticket parent post ID.
			 * @param Tribe__Tickets__Ticket_Object $ticket   The ticket that was just added.
			 * @param array                         $raw_data The ticket data that was used to save.
			 * @param string                        $class    The Commerce engine class name.
			 * @param bool                          $update   Whether the ticket is being updated or created.
			 */
			do_action( 'tribe_tickets_ticket_add', $post_id, $ticket, $data, __CLASS__, $update );

			if ( $update ) {
				/**
				 * Fired once a ticket has been updated.
				 *
				 * @since 5.8.0
				 *
				 * @param int                           $post_id  The ticket parent post ID.
				 * @param Tribe__Tickets__Ticket_Object $ticket   The ticket that was just added.
				 * @param array                         $raw_data The ticket data that was used to save.
				 * @param string                        $class    The Commerce engine class name.
				 */
				do_action( 'tec_tickets_ticket_update', $post_id, $ticket, $data, __CLASS__ );
			} else {
				/**
				 * Fired once a ticket has been created.
				 *
				 * @since 5.8.0
				 *
				 * @param int                           $post_id  The ticket parent post ID.
				 * @param Tribe__Tickets__Ticket_Object $ticket   The ticket that was just added.
				 * @param array                         $raw_data The ticket data that was used to save.
				 * @param string                        $class    The Commerce engine class name.
				 */
				do_action( 'tec_tickets_ticket_add', $post_id, $ticket, $data, __CLASS__ );
			}

			$tickets_handler->toggle_manual_update_flag( false );

			$post = get_post( $post_id );

			// If ticket start date is not set, set it to the post date.
			if ( empty( $data['ticket_start_date'] ) ) {
				$date = strtotime( $post->post_date );
				$date = date( 'Y-m-d 00:00:00', $date );

				update_post_meta( $ticket->ID, $tickets_handler->key_start_date, $date );
			}

			/*
			 * If the ticket end date has not been set and we have an event,
			 * set the ticket end date to the event start date.
			 */
			if ( empty( $data['ticket_end_date'] ) && 'tribe_events' === $post->post_type ) {
				$event_start = get_post_meta( $post_id, '_EventStartDate', true );
				update_post_meta( $ticket->ID, $tickets_handler->key_end_date, $event_start );
			}

			/** @var Tribe__Tickets__Version $version */
			$version = tribe( 'tickets.version' );

			$version->update( $ticket->ID );

			/**
			 * Fires once a ticket's data has been saved.
			 *
			 * @since 5.20.0
			 *
			 * @param int    $ticket_id The ticket id that was just added.
			 * @param int    $post_id   The ticket parent post ID.
			 * @param array  $raw_data  The ticket data that was used to save.
			 * @param string $class     The Commerce engine class name.
			 */
			do_action( 'tec_tickets_ticket_upserted', $ticket->ID, $post_id, $data, __CLASS__ );

			$this->clear_ticket_cache_for_post( $post_id );

			return $save_ticket;
		}

		/**
		 * Get the saved or default ticket provider, if active.
		 *
		 * Will return False if there is a saved provider that is currently not active.
		 * Example: If provider is WooCommerce Ticket but ETP is inactive, will return False.
		 *
		 * @see get_event_ticket_provider_object()
		 *
		 * @since 4.7
		 * @since 4.12.3 Now returning false if the provider is not active.
		 *
		 * @param int $event_id The post ID of the event to which the ticket is attached.
		 *
		 * @return string|false The ticket object class name, or false if not active.
		 */
		public static function get_event_ticket_provider( $event_id = null ) {
			$provider = static::get_event_ticket_provider_object( $event_id );

			if ( empty( $provider ) ) {
				return false;
			}

			return $provider->class_name;
		}

		/**
		 * Given a post ID, get the active providers used for RSVP(s)/ticket(s).
		 *
		 * @see get_ticket_provider_instance()
		 *
		 * @since 5.1.1
		 *
		 * @param int  $post_id          The post ID of the post/event to which RSVP(s)/ticket(s) are attached.
		 * @param bool $return_instances Whether to return instances, otherwise it will return class name strings.
		 *
		 * @return string[]|self[] Instances or names of provider classes for RSVP(s)/ticket(s) attached to the post/event.
		 */
		public static function get_active_providers_for_post( $post_id, $return_instances = false ) {
			$all_active_modules = array_keys( self::modules() );

			$active_providers = [];

			// Determine which providers have tickets for this event.
			foreach ( $all_active_modules as $module ) {
				$provider = self::get_ticket_provider_instance( $module );

				// Skip this provider if the instance couldn't be set up.
				if ( ! $provider ) {
					continue;
				}

				// Get the tickets for this event on this provider, if any.
				$tickets_orm = tribe_tickets( $provider->orm_provider );
				$tickets_orm->by( 'event', $post_id );

				if ( 0 < $tickets_orm->found() ) {
					$provider_class = $provider->class_name;

					// Check whether to return the provider class names.
					if ( ! $return_instances ) {
						$provider = $provider_class;
					}

					$active_providers[ $provider_class ] = $provider;
				}
			}

			return $active_providers;
		}

		/**
		 * Given a post ID, get the instance of the saved or default ticket provider class.
		 *
		 * Will return False if there is a saved provider that is currently not active.
		 * Example: If provider is WooCommerce Ticket but ETP is inactive, will return False.
		 *
		 * @see get_ticket_provider_instance()
		 *
		 * @since 4.12.3
		 *
		 * @param int $post_id The post ID of the event to which the ticket is attached.
		 *
		 * @return self|false Instance of child class (if confirmed active) or False if provider is not active.
		 */
		public static function get_event_ticket_provider_object( $post_id = null ) {
			/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
			$tickets_handler = tribe( 'tickets.handler' );

			// 'Tribe__Tickets__RSVP' unless filtered.
			$provider = self::get_default_module();

			// If post ID is set and a value has been saved.
			if ( ! empty( $post_id ) ) {
				$saved = get_post_meta( $post_id, $tickets_handler->key_provider_field, true );

				if ( ! empty( $saved ) ) {
					$provider = $saved;
				}
			}

			return static::get_ticket_provider_instance( $provider );
		}

		/**
		 * Given a provider string (class module name or slug), get its class instance if an active module.
		 *
		 * @param self|string $provider Examples: 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main', 'woo', 'rsvp', etc.
		 *
		 * @return self|false Instance of child class (if confirmed active) or False if provider is not active.
		 */
		public static function get_ticket_provider_instance( $provider ) {
			$is_provider_active = tribe_tickets_is_provider_active( $provider );

			if ( empty( $is_provider_active ) ) {
				return false;
			}

			if ( $provider instanceof self ) {
				return $provider;
			}

			/** @var Tribe__Tickets__Status__Manager $status */
			$status = tribe( 'tickets.status' );

			$provider = $status->get_provider_class_from_slug( $provider );

			$instance = tribe_get_class_instance( $provider );

			if ( ! $instance instanceof self ) {
				return false;
			}

			return $instance;
		}

		/**
		 * Get currency symbol
		 *
		 * @since 4.7.1
		 *
		 * @return string
		 */
		public function get_currency() {
			/**
			 * Default currency value for Tickets.
			 *
			 * @since 4.7.1
			 *
			 * @return string
			 */
			return (string) apply_filters( 'tribe_tickets_default_currency', 'USD' );
		}


		/**
		 * Returns all the tickets currently in the users cart.
		 *
		 * @since 4.9
		 *
		 * @param array $tickets
		 *
		 * @return array
		 */
		public function get_tickets_in_cart( $tickets ) {
			return $tickets;
		}

		/**
		 * Return whether we're currently on the checkout page for this Merchant.
		 *
		 * @since 4.9
		 *
		 * @return bool
		 */
		public function is_checkout_page() {
			return false;
		}

		/**
		 * If tickets exist in the cart for which we don't have meta info,
		 * redirect to the meta collection screen.
		 *
		 * @since 4.9
		 * @since 5.0.2 Correct provider attendee object.
		 *
		 * @param string|null $redirect URL to redirect to.
		 * @param null|int    $post_id  Post ID for cart.
		 */
		public function maybe_redirect_to_attendees_registration_screen( $redirect = null, $post_id = null ) {

			// Bail if the meta storage class doesn't exist
			if ( ! class_exists( 'Tribe__Tickets_Plus__Meta__Storage' ) ) {
				return;
			}

			if ( ! class_exists( 'Tribe__Tickets_Plus__Main' ) ) {
				return;
			}

			// They're submitting RSVPs, do not include them for now
			if ( ! empty( $_POST['tribe_tickets_rsvp_submission'] ) ) {
				return;
			}

			/**
			 * This Try/Catch is present to deal with a problem on Autoloading from version 5.1.0 ET+ with ET 5.0.3.
			 *
			 * @todo Needs to be revised once proper autoloading rules are done for Common, ET and ET+.
			 */
			try {
				/** @var \Tribe__Tickets__Attendee_Registration__Main $attendee_registration */
				$attendee_registration = tribe( 'tickets.attendee_registration' );
			} catch( RuntimeException $error ) {
				return;
			}

			if (
				$attendee_registration->is_on_page()
				|| $attendee_registration->is_cart_rest()
				|| $attendee_registration->is_using_shortcode()
			) {
				return;
			}

			// Return if not trying to access the checkout page
			if ( ! $this->is_checkout_page() ) {
				return;
			}

			$q_provider = tribe_get_request_var( 'provider', false );

			// Provider to use the attendee object.
			if (
				static::class === $q_provider
				|| empty( $q_provider )
			) {
				$q_provider = $this->attendee_object;
			}

			/**
			 * Filter to add/remove tickets from the global cart
			 *
			 * @since 4.9
			 * @since 4.11.0 Added $q_provider to allow context of current provider.
			 *
			 * @param array  $tickets_in_cart The array containing the cart elements. Format array( 'ticket_id' => 'quantity' ).
			 * @param string $q_provider      Current ticket provider.
			 */
			$tickets_in_cart = apply_filters( 'tribe_tickets_tickets_in_cart', [], $q_provider );

			// Bail if there are no tickets
			if ( empty( $tickets_in_cart ) ) {
				return;
			}

			/** @var Tribe__Tickets_Plus__Meta $meta */
			$meta = tribe( 'tickets-plus.meta' );

			$cart_has_meta = true;

			// If the method exists (latest ET+ version), run it.
			if ( method_exists( $meta, 'cart_has_meta' ) ) {
				$cart_has_meta = $meta->cart_has_meta( $tickets_in_cart );
			}

			// There are no meta fields on the cart tickets.
			if ( ! $cart_has_meta ) {
				return;
			}

			/** @var \Tribe__Tickets_Plus__Meta__Contents $meta_contents */
			$meta_contents = tribe( 'tickets-plus.meta.contents' );

			$up_to_date = $meta_contents->is_stored_meta_up_to_date( $tickets_in_cart );

			// There are no updates to perform on ticket meta.
			if ( $up_to_date ) {
				return;
			}

			$url = $attendee_registration->get_url();

			if ( ! empty( $q_provider ) ) {
				$provider_slug = tribe_tickets_get_provider_query_slug();
				$url = add_query_arg( $provider_slug, $q_provider, $url );
			}

			if ( ! empty( $redirect ) ) {
				$storage = new Tribe__Tickets_Plus__Meta__Storage();

				$key = $storage->store_temporary_data( $redirect );

				/** @var \Tribe__Tickets__Commerce__PayPal__Main $commerce_paypal */
				$commerce_paypal = tribe( 'tickets.commerce.paypal' );

				$url = add_query_arg(
					[
						'event_tickets_redirect_to' => $key,
						'provider'                  => $commerce_paypal->attendee_object,
					],
					$url
				);
			}

			// Pass post ID to URL if set.
			if ( null !== $post_id ) {
				$url = add_query_arg( 'tribe_tickets_post_id', $post_id, $url );
			}

			wp_safe_redirect( $url );
			exit;
		}

		/**
		 * Get list of tickets in cart for a specific provider.
		 *
		 * @since 5.0.3
		 *
		 * @param null|string|false $provider The provider slug or false if no provider, leave as null to detect from page.
		 *
		 * @return array List of tickets in cart for the provider.
		 */
		public static function get_tickets_in_cart_for_provider( $provider = null ) {
			if ( null === $provider ) {
				$provider = tribe_get_request_var( 'provider', false );
			}

			/**
			 * Filter to add/remove tickets from the global cart.
			 *
			 * @since 4.9
			 * @since 4.11.0 Added $provider to allow context of current provider.
			 *
			 * @param array        $tickets_in_cart The array containing the cart elements. Format array( 'ticket_id' => 'quantity' ).
			 * @param string|false $provider        Current ticket provider or false if not set.
			 */
			return (array) apply_filters( 'tribe_tickets_tickets_in_cart', [], $provider );
		}

		/**
		 * Generates the security code that will be used for printed tickets and QR codes.
		 *
		 * @since 4.7
		 *
		 * @param string $attendee_id The attendee ID or another string to based the security code off of.
		 *
		 * @return string The generated security code.
		 */
		public function generate_security_code( $attendee_id ) {
			return substr( md5( wp_rand() . '_' . $attendee_id ), 0, 10 );
		}

		/**
		 * Create an attendee for the Commerce provider from a ticket.
		 *
		 * @since 5.1.0
		 *
		 * @param Tribe__Tickets__Ticket_Object|int $ticket        Ticket object or ID to create the attendee for.
		 * @param array                             $attendee_data Attendee data to create from.
		 *
		 * @return WP_Post|false The new post object or false if unsuccessful.
		 */
		public function create_attendee( $ticket, $attendee_data ) {
			// Get the ticket object from the ID.
			if ( is_numeric( $ticket ) ) {
				$ticket = $this->get_ticket( 0, (int) $ticket );
			}

			// If the ticket is not valid, stop creating the attendee.
			if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
				return false;
			}

			/** @var Tribe__Tickets__Attendee_Repository $orm */
			$orm = tribe_attendees( $this->orm_provider );

			try {
				return $orm->create_attendee_for_ticket( $ticket, $attendee_data );
			} catch ( Tribe__Repository__Usage_Error $e ) {
				do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );
				return false;
			}
		}

		/**
		 * Update an attendee for the Commerce provider.
		 *
		 * @since 5.1.0
		 *
		 * @param array|int $attendee      The attendee data or ID for the attendee to update.
		 * @param array     $attendee_data The attendee data to update to.
		 *
		 * @return WP_Post|false The updated post object or false if unsuccessful.
		 */
		public function update_attendee( $attendee, $attendee_data ) {
			if ( is_numeric( $attendee ) ) {
				$attendee_id = (int) $attendee;
			} elseif ( is_array( $attendee ) && isset( $attendee['attendee_id'] ) ) {
				$attendee_id = (int) $attendee['attendee_id'];
			} else {
				return false;
			}

			// Set the attendee ID to be updated.
			$attendee_data['attendee_id'] = $attendee_id;

			/** @var Tribe__Tickets__Attendee_Repository $orm */
			$orm = tribe_attendees( $this->orm_provider );

			try {
				$attendee = $orm->update_attendee( $attendee_data );
			} catch ( Tribe__Repository__Usage_Error $e ) {
				do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );
				return false;
			}

			return $attendee;
		}

		/**
		 * Maybe lookup or create an attendee user from an email.
		 *
		 * @since 5.1.0
		 *
		 * @param string $email The email to maybe set up the user from.
		 * @param array  $args  The arguments used from this attendee.
		 *
		 * @return int|null The user ID or null if not set up.
		 */
		public function maybe_setup_attendee_user_from_email( $email, $args = [] ) {
			if ( empty( $email ) || ! is_email( $email ) ) {
				return null;
			}

			$lookup_user_from_email = Arr::get( $args, 'use_existing_user', true );
			$create_user_from_email = Arr::get( $args, 'create_user', false );
			$send_new_user_info     = Arr::get( $args, 'send_email', false );

			/**
			 * Allow filtering whether to enable user lookups by Attendee Email.
			 *
			 * @since 5.1.0
			 *
			 * @param bool  $lookup_user_from_email Whether to lookup the User using the Attendee Email if User ID is not set.
			 * @param array $args                   The arguments being set for this attendee.
			 */
			$lookup_user_from_email = (bool) apply_filters( 'tribe_tickets_attendee_lookup_user_from_email', $lookup_user_from_email, $args );

			if ( $lookup_user_from_email ) {
				// Check if user exists.
				$user = get_user_by( 'email', $email );

				if ( $user ) {
					return $user->ID;
				}
			}

			/**
			 * Allow filtering whether to enable creating users using the Attendee Email.
			 *
			 * @since 5.1.0
			 *
			 * @param bool  $create_user_from_email Whether to create the User using the Attendee Email if User ID is not set.
			 * @param array $args                   The arguments being set for this attendee.
			 */
			$create_user_from_email = (bool) apply_filters( 'tribe_tickets_attendee_create_user_from_email', $create_user_from_email, $args );

			// Do not create the user from the email.
			if ( ! $create_user_from_email ) {
				return null;
			}

			// Create the user using the attendee email.
			$created = wp_create_user( $email, wp_generate_password( 12, false ), $email );

			// The user was not created successfully.
			if ( ! $created || is_wp_error( $created ) ) {
				return null;
			}

			// Set user details.
			$user_details = [
				'display_name' => Arr::get( $args, 'display_name', null ),
				'first_name'   => Arr::get( $args, 'first_name', null ),
				'last_name'    => Arr::get( $args, 'last_name', null ),
			];

			$user_details = array_filter( $user_details );

			// Save user details if we have any.
			if ( ! empty( $user_details ) ){
				$user_details['ID'] = $created;

				wp_update_user( $user_details );
			}

			/**
			 * Allow filtering whether to send the new user information email to the new user.
			 *
			 * @since 5.1.0
			 *
			 * @param bool  $send_new_user_info Whether to send the new user information email to the new user.
			 * @param array $args               The arguments being set for this attendee.
			 */
			$send_new_user_info = (bool) apply_filters( 'tribe_tickets_attendee_create_user_from_email_send_new_user_info', $send_new_user_info, $args );

			if ( $send_new_user_info ) {
				wp_send_new_user_notifications( $created, 'user' );
			}

			return $created;
		}

		/**
		 * Localized messages for errors, etc in javascript. Added in assets() above.
		 * Set up this way to amke it easier to add messages as needed.
		 *
		 * @since 4.11.0
		 *
		 * @return array
		 */
		public static function set_messages() {
			return [
				'api_error_title'        => _x( 'API Error', 'Error message title, will be followed by the error code.', 'event-tickets' ),
				'connection_error'       => __( 'Refresh this page or wait a few minutes before trying again. If this happens repeatedly, please contact the Site Admin.', 'event-tickets' ),
				'capacity_error'         => __( 'The ticket for this event has sold out and has been removed from your cart.', 'event-tickets' ),
				'validation_error_title' => __( 'Whoops!', 'event-tickets' ),
				'validation_error'       => '<p>' . sprintf( esc_html_x( 'You have %s ticket(s) with a field that requires information.', 'The %s will change based on the error produced.', 'event-tickets' ), '<span class="tribe-tickets__notice--error__count">0</span>' ) . '</p>',
			];
		}

		/**
		 * Return the string representation of this provider class as the class name for backwards compatibility.
		 *
		 * @since 4.12.3
		 *
		 * @return string The class name.
		 */
		public function __toString() {
			return $this->class_name;
		}

		/**
		 * Removes this module from the list of active modules.
		 *
		 * @since 5.8.0
		 *
		 * @return void This module is removed from the list of active modules, if it was active.
		 */
		public function deactivate(): void {
			unset( self::$active_modules[ get_class( $this ) ] );
		}

		/**
		 * Filter provider information for the admin tickets table.
		 *
		 * @since 5.14.0
		 *
		 * @param array[] $provider_info The list of provider information.
		 *
		 * @return array[] The filtered list of provider information.
		 */
		public function filter_admin_tickets_table_provider_info( $provider_info ) {
			$provider_info[ $this->class_name ] = [
				'title'              => $this->plugin_name,
				'event_meta_key'     => $this->get_event_key(),
				'attendee_post_type' => $this->attendee_object,
				'ticket_post_type'   => $this->ticket_object,
			];

			return $provider_info;
		}

		/************************
		 *                      *
		 *  Deprecated Methods  *
		 *                      *
		 ************************/
		// @codingStandardsIgnoreStart

		/**
		 * Tests if the user has the specified capability in relation to whatever post type
		 * the attendee object relates to.
		 *
		 * For example, if the attendee was generated for a ticket set up in relation to a
		 * post of the banana type, the generic capability "edit_posts" will be mapped to
		 * "edit_bananas" or whatever is appropriate.
		 *
		 * @internal for internal plugin use only (in spite of having public visibility)
		 *
		 * @deprecated  4.6.2
		 *
		 * @see    tribe( 'tickets.attendees' )->user_can
		 *
		 * @param string $generic_cap
		 * @param int    $attendee_id
		 *
		 * @return boolean
		 */
		public function user_can( $generic_cap, $attendee_id ) {
			_deprecated_function( __METHOD__, '4.6.2', 'tribe( "tickets.metabox" )->user_can( $generic_cap, $attendee_id )' );
			return tribe( 'tickets.metabox' )->user_can( $generic_cap, $attendee_id );
		}

		/**
		 * Check and set global capacity options for the "event" post
		 *
		 * @deprecated 4.6.2
		 * @since 4.6
		 *
		 * @return object ajax success object
		 */
		public function edit_global_capacity_level() {
			_deprecated_function( __METHOD__, '4.6.2', 'tribe_tickets_update_capacity' );
		}

		/**
		 * Sets an AJAX error, returns a JSON array and ends the execution.
		 *
		 * @deprecated 4.6.2
		 *
		 * @param string $message
		 */
		final protected function ajax_error( $message = '' ) {
			_deprecated_function( __METHOD__, '4.6.2', 'wp_send_json_error()' );
			wp_send_json_error( $message );
		}

		/**
		 * Sets an AJAX response, returns a JSON array and ends the execution.
		 *
		 * @deprecated 4.6.2
		 *
		 * @param mixed $data
		 */
		final protected function ajax_ok( $data ) {
			_deprecated_function( __METHOD__, '4.6.2', 'wp_send_json_success()' );
			wp_send_json_success( $data );
		}

		// @codingStandardsIgnoreEnd
	}
}
