<?php

use Tribe__Cache_Listener as Cache;

if ( ! class_exists( 'Tribe__Tickets__Ticket_Object' ) ) {
	/**
	 *    Generic object to hold information about a single ticket
	 */
	class Tribe__Tickets__Ticket_Object {
		/**
		 * This value - an empty string - should be used to populate the stock
		 * property in situations where no limit has been placed on stock
		 * levels.
		 */
		const UNLIMITED_STOCK = '';

		/**
		 * Unique identifier
		 * @var int
		 */
		public $ID;
		/**
		 * Name of the ticket
		 * @var string
		 */
		public $name;

		/**
		 * Free text with a description of the ticket
		 * @var string
		 */
		public $description;

		/**
		 * Whether to show the description on the front end and in emails
		 *
		 * @since 4.6
		 *
		 * @var boolean
		 */
		public $show_description = true;

		/**
		 * Current sale price, without any sign. Just a float.
		 *
		 * @var float
		 */
		public $price;

		/**
		 * Ticket Capacity
		 *
		 * @since 4.6
		 *
		 * @var    int
		 */
		public $capacity;

		/**
		 * Regular price (if the ticket is not on a special sale this will be identical to
		 * $price).
		 *
		 * @var float
		 */
		public $regular_price;

		/**
		 * Indicates if the ticket is currently being offered at a reduced price as part
		 * of a special sale.
		 *
		 * @var bool
		 */
		public $on_sale;

		/**
		 * Link to the admin edit screen for this ticket in the provider system,
		 * or null if the provider doesn't have any way to edit the ticket.
		 * @var string
		 */
		public $admin_link;

		/**
		 * Link to the report screen for this ticket in the provider system,
		 * or null if the provider doesn't have any sales reports.
		 * @var string
		 */
		public $report_link;

		/**
		 * Link to the front end of this ticket, if the providers has single view
		 * for this ticket.
		 * @var string
		 */
		public $frontend_link;

		/**
		 * Class name of the provider handling this ticket
		 * @var
		 */
		public $provider_class;

		/**
		 * Holds the SKU for the ticket
		 *
		 * @var string
		 */
		public $sku;

		/**
		 * Holds the IAC setting for the ticket.
		 *
		 * @since 5.0.3
		 *
		 * @var string
		 */
		public $iac = 'none';

		/**
		 * Holds the price suffix.
		 *
		 * @since 4.12.0
		 *
		 * @var string
		 */
		public $price_suffix;

		/**
		 * Holds the menu order for the ticket
		 *
		 * @since 4.6
		 *
		 * @var string
		 */
		public $menu_order;

		/**
		 * @var Tribe__Tickets__Tickets
		 */
		protected $provider;

		/**
		 * Amount of tickets of this kind in stock
		 * Use $this->stock( value ) to set manage and get the value
		 *
		 * @var mixed
		 */
		protected $stock;

		/**
		 * The mode of stock handling to be used for the ticket when global stock
		 * is enabled for the event.
		 *
		 * @var string
		 */
		protected $global_stock_mode = Tribe__Tickets__Global_Stock::OWN_STOCK_MODE;

		/**
		 * The maximum permitted number of sales for this ticket when global stock
		 * is enabled for the event and CAPPED_STOCK_MODE is in effect.
		 *
		 * @var int
		 */
		protected $global_stock_cap = 0;

		/**
		 * Amount of tickets of this kind sold
		 * Use $this->qty_sold( value ) to set manage and get the value
		 *
		 * @var int
		 */
		protected $qty_sold = 0;

		/**
		 * Number of tickets for which an order has been placed but not confirmed or "completed".
		 * Use $this->qty_pending( value ) to set manage and get the value
		 *
		 * @var int
		 */
		protected $qty_pending = 0;

		/**
		 * Number of tickets for which an order has been cancelled.
		 * Use $this->qty_cancelled( value ) to set manage and get the value
		 *
		 * @var int
		 */
		protected $qty_cancelled = 0;

		/**
		 * Number of tickets for which an order has been refunded.
		 * Use $this->$qty_refunded( value ) to set manage and get the value
		 *
		 * @since 4.7.3
		 *
		 * @var int
		 */
		protected $qty_refunded = 0;

		/**
		 * Holds whether or not stock is being managed
		 *
		 * @var boolean
		 */
		protected $manage_stock = false;

		/**
		 * Date the ticket should be put on sale
		 *
		 * @var string
		 */
		public $start_date;

		/**
		 * Time the ticket should be put on sale
		 *
		 * @since 4.6
		 *
		 * @var string
		 */
		public $start_time;

		/**
		 * Date the ticket should be stop being sold
		 * @var string
		 */
		public $end_date;

		/**
		 * Time the ticket should be stop being sold
		 *
		 * @since 4.6
		 *
		 * @var string
		 */
		public $end_time;

		/**
		 * Purchase limit for the ticket
		 *
		 * @var
		 * @deprecated 4.7.5
		 */
		public $purchase_limit;

		/**
		 * Variable used to save the DateTimeZone object of the parent event
		 *
		 * @since 4.7.1
		 *
		 * @var null|DateTimeZone
		 */
		private $event_timezone = null;

		/**
		 * ID of the parent event of the current Ticket
		 *
		 * @since 4.7.1
		 *
		 * @var null|int
		 */
		private $event_id = null;
		/**
		 * The type of ticket.
		 *
		 * E.g. 'RSVP', 'default' (for Single Tickets) or other.
		 *
		 * @since 5.6.7
		 *
		 * @var string|null
		 */
		private ?string $type;

		/**
		 * Tribe__Tickets__Ticket_Object constructor.
		 *
		 * since 5.6.4
		 *
		 * @since 5.29.0 Made $data explicitly nullable.
		 *
		 * @param array<string,mixed>|null $data The data to populate the object with, if any.
		 */
		public function __construct( ?array $data = null ) {
			if ( empty( $data ) ) {
				return;
			}

			foreach ( $data as $key => $value ) {
				$this->$key = $value;
			}
		}

		/**
		 * Get the ticket's start date
		 *
		 * @since 4.2
		 *
		 * @param bool $as_timestamp Flag to disable the default behavior and use DateTime object instead.
		 *
		 * @return string|DateTime|false|int
		 */
		public function start_date( $as_timestamp = true ) {
			$start_date = null;
			if ( ! empty( $this->start_date ) ) {
				$start_date = $this->start_date;

				if ( ! empty( $this->start_time ) ) {
					$start_date .= ' ' . $this->start_time;
				}

				$start_date = $this->get_date( $start_date, $as_timestamp );
			}
			return $start_date;
		}

		/**
		 * Get the ticket's end date
		 *
		 * @since 4.2
		 *
		 * @param bool $as_timestamp Flag to disable the default behavior and use DateTime object instead.
		 *
		 * @return string|DateTime|false|int
		 */
		public function end_date( $as_timestamp = true ) {
			$end_date = null;

			if ( ! empty( $this->end_date ) ) {
				$end_date = $this->end_date;

				if ( ! empty( $this->end_time ) ) {
					$end_date .= ' ' . $this->end_time;
				}

				$end_date = $this->get_date( $end_date, $as_timestamp );
			}

			return $end_date;
		}

		/**
		 * Determines if the given date is within the ticket's start/end date range
		 *
		 * @param string|int|null $datetime The date/time that we want to determine if it falls within the start/end date range.
		 *
		 * @return boolean Whether or not the provided date/time falls within the start/end date range
		 */
		public function date_in_range( $datetime = null ) {
			$date = $this->get_date( $datetime, false );

			$start = $this->start_date( false );
			$end   = $this->end_date( false );

			if ( ! $start instanceof DateTime ) {
				$start = $this->start_date();
			}

			if ( ! $end instanceof DateTime ) {
				$end = $this->end_date();
			}

			// Bail if we don't have an end date and the event has passed
			// Check if the event has passed in case we're using TEC
			$is_past_event = false;

			$event = $this->get_event();

			if ( function_exists( 'tribe_is_past_event' ) && $event instanceof WP_Post && tribe_is_event( $event ) ) {
				$is_past_event = tribe_is_past_event( $event );
			}

			if ( empty( $end ) && $is_past_event ) {
				return false;
			}

			return (
				(
					empty( $start )
					|| $start <= $date
				)
				&& (
					empty( $end )
					|| $date <= $end
				)
			);
		}


		/**
		 * Get a DateTime object or a Timestamp date representation of the object, if the DateTime object is used
		 * the timezone from the event associated with the ticket is going to be used to have a more accurate
		 * timestamp
		 *
		 * @since 4.7.1
		 *
		 * @param string $date
		 * @param bool $as_timestamp
		 *
		 * @return DateTime|false|int
		 */
		public function get_date( $date = '', $as_timestamp = true ) {
			if ( '' === $date ) {
				return false;
			}

			if ( null === $date ) {
				$date = time();
			}

			try {
				$timezone = $this->get_event_timezone();

				$datetime = Tribe__Date_Utils::build_date_object( $date, $timezone );

				if ( Tribe__Date_Utils::is_timestamp( $datetime ) ) {
					$datetime = Tribe__Date_Utils::build_date_object( $datetime->format( Tribe__Date_Utils::DBDATETIMEFORMAT ), $timezone );
				}
			} catch ( Exception $exception ) {
				return strtotime( $date );
			}

			if ( $as_timestamp ) {
				return $datetime->getTimestamp();
			}

			return $datetime;
		}


		/**
		 * Return a DateTimeZone associated with the parent Event of the current ticket
		 *
		 * @since 4.7.1
		 *
		 * @return DateTimeZone|null
		 */
		public function get_event_timezone() {
			if (
				class_exists( 'Tribe__Events__Timezones' )
				&& ! is_null( $this->get_event_id() )
				&& is_null( $this->event_timezone )
			) {
				try {
					$timezone_string = Tribe__Events__Timezones::get_event_timezone_string( $this->get_event_id() );

					$this->event_timezone = new DateTimeZone( $timezone_string );
				} catch ( Exception $exception ) {
					$this->event_timezone = 'UTC+0' === $timezone_string ? new DateTimeZone( 'UTC' ) : null;
				}
			}

			if ( null === $this->event_timezone ) {
				$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 );
				}
				$this->event_timezone = new DateTimeZone( $wp_timezone );
			}

			return $this->event_timezone;
		}

		/**
		 * Determines if the given date is before the ticket's start date
		 *
		 * @param null|string $datetime The date/time that we want to compare to the ticket's start date
		 *
		 * @return boolean Whether or not the provided date/time is before than the ticket's start date
		 */
		public function date_is_earlier( $datetime = null ) {
			$date = $this->get_date( $datetime, false );

			$start = $this->start_date( false );

			if ( ! $start instanceof DateTime ) {
				$start = $this->start_date();
			}

			return ! empty( $start ) && $date < $start;
		}

		/**
		 * Determines if the given date is after the ticket's end date
		 *
		 * @param null|string $datetime The date/time that we want to compare to the ticket's start date
		 *
		 * @return boolean Whether or not the provided date/time is after than the ticket's end date
		 */
		public function date_is_later( $datetime = null ) {
			$date = $this->get_date( $datetime, false );

			$end = $this->end_date( false );

			if ( ! $end instanceof DateTime ) {
				$end = $this->end_date();
			}

			return ! empty( $end ) && $date > $end;
		}

		/**
		 * Returns ticket availability slug
		 *
		 * The availability slug is used for CSS class names and filter helper strings
		 *
		 * @since 4.2
		 * @param int (null) $datetime the timestamp to test
		 *
		 * @return string
		 */
		public function availability_slug( $datetime = null ) {
			$slug = 'available';

			if ( $this->date_is_earlier( $datetime ) ) {
				$slug = 'availability-future';
			} elseif ( $this->date_is_later( $datetime ) ) {
				$slug = 'availability-past';
			}

			/**
			 * Filters the availability slug
			 *
			 * @param string Slug
			 * @param string Datetime string
			 */
			$slug = apply_filters( 'event_tickets_availability_slug', $slug, $datetime );

			return $slug;
		}

		/**
		 * Provides the quantity of original stock of tickets
		 *
		 * @deprecated 4.6
		 *
		 * @return int
		 */
		public function original_stock() {
			return $this->capacity();
		}

		/**
		 * Determines if there is any stock for purchasing
		 *
		 * @return boolean
		 */
		public function is_in_stock() {
			// If we aren't tracking stock, then always assume it is in stock.
			if ( ! $this->managing_stock() ) {
				return true;
			}

			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$cache_key   = __METHOD__ . '-' . $this->ID;
			$is_ticket_cache_enabled = $this->is_ticket_cache_enabled();

			if ( $is_ticket_cache_enabled ) {
				$cached = $cache->get( $cache_key, Cache::TRIGGER_SAVE_POST, null );

				if ( $cached !== null && is_string( $cached ) && in_array( $cached, [ 'yes', 'no' ], true ) ) {
					$is_in_stock = tribe_is_truthy( $cached );
					$cache->set( $cache_key, $is_in_stock ? 'yes' : 'no', 0, Cache::TRIGGER_SAVE_POST );

					return $is_in_stock;
				}
			}

			$remaining    = $this->inventory();
			$is_unlimited = - 1 === $remaining;

			$is_in_stock = false === $remaining || $remaining > 0 || $is_unlimited;

			if( $is_ticket_cache_enabled ) {
				$cache->set( $cache_key, $is_in_stock ? 'yes' : 'no', 0, Cache::TRIGGER_SAVE_POST );
			}

			return $is_in_stock;
		}

		/**
		 * Returns whether or not the ticket is managing stock
		 *
		 * @param boolean $manages_stock Boolean to set stock management state
		 * @return boolean
		 */
		public function manage_stock( $manages_stock = null ) {
			if ( null !== $manages_stock ) {
				$this->manage_stock = tribe_is_truthy( $manages_stock );
			}

			return $this->manage_stock;
		}

		/**
		 * Returns whether or not the ticket is managing stock. Alias method with a friendlier name for fetching state.
		 *
		 * @param boolean $manages_stock Boolean to set stock management state
		 * @return boolean
		 */
		public function managing_stock( $manages_stock = null ) {
			return $this->manage_stock( $manages_stock );
		}

		/**
		 * Provides the Inventory of the Ticket which should match the Commerce Stock
		 *
		 * @since 4.6
		 * @since 4.12.3 Account for possibly inactive ticket provider.
		 *
		 * @return int
		 */
		public function inventory() {
			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$cache_key   = __METHOD__ . '-' . $this->ID;
			$is_ticket_cache_enabled = $this->is_ticket_cache_enabled();

			if ( $is_ticket_cache_enabled ) {
				$cached = $cache->get( $cache_key, Cache::TRIGGER_SAVE_POST, null );

				if ( $cached && is_int( $cached ) ) {
					return $cached;
				}
			}

			// Fetch provider (also sets if found).
			$provider = $this->get_provider();

			$capacity = $this->capacity();

			// If we don't have the provider, get the result from inventory.
			if ( empty( $provider ) ) {
				/**
				 * Filters the inventory of the ticket.
				 *
				 * @since 5.16.0
				 *
				 * @param int $inventory The inventory of the ticket.
				 * @param Tribe__Tickets__Ticket_Object $ticket The ticket object.
				 * @param array<string,mixed>           $event_attendees The event attendees.
				 * @param array<string,mixed>           $attendees       The ticket attendees.
				 */
				$inventory = apply_filters( 'tribe_tickets_ticket_inventory', $capacity - $this->qty_sold() - $this->qty_pending(), $this, [], [] );

				if ( $is_ticket_cache_enabled ) {
					$cache->set( $cache_key, $inventory, 0, Cache::TRIGGER_SAVE_POST );
				}

				return $inventory;
			}

			// If we aren't tracking stock, then always assume it is in stock or capacity is unlimited.
			if ( ! $this->managing_stock() || - 1 === $capacity ) {
				if ( $is_ticket_cache_enabled ) {
					$cache->set( $cache_key, - 1, 0, Cache::TRIGGER_SAVE_POST );
				}

				return - 1;
			}

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

			// Fetch the Attendees
			$attendees       = $provider->get_attendees_by_id( $this->ID );
			$attendees_count = 0;
			$not_going_arr   = $status_mgr->get_statuses_by_action( 'count_not_going', 'rsvp' );

			// Loop on All the attendees, allowing for some filtering of which will be removed or not
			foreach ( $attendees as $attendee ) {
				// Prevent RSVP with Not Going Status to decrease Inventory
				if ( ! empty( $attendee['provider_slug'] ) && 'rsvp' === $attendee['provider_slug'] && in_array( $attendee['order_status'], $not_going_arr, true ) ) {
					continue;
				}

				// allow providers to decide if an attendee will count toward inventory decrease or not
				if ( ! $provider->attendee_decreases_inventory( $attendee ) ) {
					continue;
				}

				$attendees_count++;
			}

			// Do the math!
			$inventory[] = $capacity - $attendees_count;

			// Calculate and verify the Event Inventory
			if (
				Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE === $this->global_stock_mode()
				|| Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE === $this->global_stock_mode()
			) {
				$event_id              = $this->get_event()->ID;
				$event_attendees       = $provider->get_attendees_by_id( $event_id );
				$event_attendees_count = 0;

				foreach ( $event_attendees as $attendee ) {
					$attendee_ticket_stock = new Tribe__Tickets__Global_Stock( $attendee['event_id'] );
					// Bypass any potential weirdness (RSVPs or such).
					if ( empty( $attendee['product_id'] ) || (int) $attendee['event_id'] !== (int) $event_id ) {
						continue;
					}

					$attendee_ticket_stock_mode = get_post_meta( $attendee['product_id'], Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, true );

					// On all cases of indy stock we don't add
					if (
						! $attendee_ticket_stock->is_enabled()
						|| empty( $attendee_ticket_stock_mode )
						|| ! $provider->attendee_decreases_inventory( $attendee )
						|| Tribe__Tickets__Global_Stock::OWN_STOCK_MODE === $attendee_ticket_stock_mode
					) {
						continue;
					}

					// All the others we add to the count
					$event_attendees_count++;
				}

				$inventory[] = tribe_tickets_get_capacity( $event_id ) - $event_attendees_count;
			}

			$inventory = min( $inventory );
			// Prevents Negative
			$inventory = max( $inventory, 0 );

			/**
			 * Filters the inventory of the ticket.
			 *
			 * @since 5.16.0
			 *
			 * @param int                           $inventory       The inventory of the ticket.
			 * @param Tribe__Tickets__Ticket_Object $ticket          The ticket object.
			 * @param array<string,mixed>           $event_attendees The event attendees.
			 * @param array<string,mixed>           $attendees       The ticket attendees.
			 */
			$inventory = apply_filters(
				'tribe_tickets_ticket_inventory',
				$inventory,
				$this,
				$event_attendees ?? [],
				$attendees ?? []
			);

			if ( $is_ticket_cache_enabled ) {
				$cache->set( $cache_key, $inventory, 0, Cache::TRIGGER_SAVE_POST );
			}

			return $inventory;
		}

		/**
		 * Provides the quantity of remaining tickets
		 *
		 * @deprecated   4.6  We are now using inventory as the new Remaining
		 *
		 * @return int
		 */
		public function remaining() {
			return $this->inventory();
		}

		/**
		 * Provides the quantity of Available tickets based on the Attendees number
		 *
		 * @todo   Create a way to get the Available for an Event (currently impossible)
		 *
		 * @see \Tribe__Tickets__Tickets_Handler::get_ticket_max_purchase() Use instead to get the front-end quantity.
		 *
		 * @since 4.6
		 *
		 * @return int
		 */
		public function available() {
			// if we aren't tracking stock, then always assume it is in stock or capacity is unlimited.
			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$cache_key   = __METHOD__ . '-' . $this->ID;
			$is_ticket_cache_enabled = $this->is_ticket_cache_enabled();

			if ( $is_ticket_cache_enabled ) {
				$cached = $cache->get( $cache_key, Cache::TRIGGER_SAVE_POST, null );

				if ( is_int( $cached ) && $cached >= 0 ) {
					return $cached;
				}
			}

			// If not managing stock or the capacity is unlimited, then the availability is unlimited.
			if ( ! $this->managing_stock() || - 1 === $this->capacity() ) {
				if ( $is_ticket_cache_enabled ) {
					$cache->set( $cache_key, - 1, 0, Cache::TRIGGER_SAVE_POST );
				}

				return - 1;
			}

			$values[] = $this->inventory();
			$values[] = $this->capacity();
			$values[] = $this->stock();

			// Whatever is the lowest we use it
			$available = min( $values );

			// Prevents Negative
			$available = max( $available, 0 );

			if ( $is_ticket_cache_enabled ) {
				$cache->set( $cache_key, $available, 0, Cache::TRIGGER_SAVE_POST );
			}

			return $available;
		}

		/**
		 * Gets the Capacity for the Ticket
		 *
		 * @since 4.6
		 *
		 * @return  int
		 */
		public function capacity() {
			if ( ! $this->managing_stock() ) {
				return '';
			}

			/** @var Tribe__Cache $cache */
			$cache = tribe( 'cache' );
			$cache_key = __METHOD__ . '-' . $this->ID;
			$is_ticket_cache_enabled = $this->is_ticket_cache_enabled();

			if ( $is_ticket_cache_enabled ) {
				$cached = $cache->get( $cache_key, Cache::TRIGGER_SAVE_POST, null );

				if ( is_int( $cached ) && $cached >= 0 ) {
					return $cached;
				}
			}

			if ( is_null( $this->capacity ) ) {
				$this->capacity = tribe_tickets_get_capacity( $this->ID );
			}

			$stock_mode = $this->global_stock_mode();

			// Unlimited is always unlimited
			if ( - 1 === (int) $this->capacity ) {
				if ( $is_ticket_cache_enabled ) {
					$cache->set( $cache_key, - 1, 0, Cache::TRIGGER_SAVE_POST );
				}

				return - 1;
			}

			// If Capped or we used the local Capacity
			if (
				Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE === $stock_mode
				|| Tribe__Tickets__Global_Stock::OWN_STOCK_MODE === $stock_mode
			) {
				if ( $is_ticket_cache_enabled ) {
					$cache->set( $cache_key, (int) $this->capacity, 0, Cache::TRIGGER_SAVE_POST );
				}

				return (int) $this->capacity;
			}

			$event_capacity = (int) tribe_tickets_get_capacity( $this->get_event() );

			if ( $is_ticket_cache_enabled ) {
				$cache->set( $cache_key, $event_capacity, 0, Cache::TRIGGER_SAVE_POST );
			}

			return $event_capacity;
		}

		/**
		 * Method to manage the protected `stock` property of the Object
		 * Prevents setting `stock` lower then zero but may return `-1`.
		 *
		 * Returns the current ticket stock level: either an integer or an
		 * empty string (Tribe__Tickets__Ticket_Object::UNLIMITED_STOCK)
		 * if stock is unlimited.
		 *
		 * @param int|null $value This will overwrite the old value
		 *
		 * @return int|string
		 */
		public function stock( $value = null ) {
			if ( null === $value ) {
				$value = null === $this->stock
					? (int) get_post_meta( $this->ID, '_stock', true )
					: $this->stock;
			}

			// if we aren't tracking stock, then always assume it is in stock or capacity is unlimited
			if (
				! $this->managing_stock()
				|| -1 === $this->capacity()
			) {
				return -1;
			}

			// If the Value was passed as numeric value overwrite
			if (
				is_numeric( $value )
				|| $value === self::UNLIMITED_STOCK
			) {
				$this->stock = $value;
			}

			// if stock is negative, force it to 0
			$this->stock = 0 >= $this->stock ? 0 : $this->stock;

			$stock[] = $this->stock;

			if (
				(
					Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE === $this->global_stock_mode()
					|| Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE === $this->global_stock_mode()
				)
				&& isset( $this->get_event()->ID )
			) {
				$stock[] = (int) get_post_meta( $this->get_event()->ID, Tribe__Tickets__Global_Stock::GLOBAL_STOCK_LEVEL, true );
			}

			// return the new Stock
			return min( $stock );
		}

		/**
		 * Sets or gets the current global stock mode in effect for the ticket.
		 *
		 * Typically this is one of the constants provided by Tribe__Tickets__Global_Stock:
		 *
		 *     GLOBAL_STOCK_MODE if it should draw on the global stock
		 *     CAPPED_STOCK_MODE as above but with a limit on the total number of allowed sales
		 *     OWN_STOCK_MODE if it should behave as if global stock is not in effect
		 *
		 * @param string $mode
		 *
		 * @return string
		 */
		public function global_stock_mode( $mode = null ) {
			if ( ! is_null( $mode ) ) {
				$this->global_stock_mode = $mode;
			}

			if ( empty( $this->global_stock_mode ) ) {
				$this->global_stock_mode = get_post_meta( $this->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, true );
			}

			return $this->global_stock_mode;
		}

		/**
		 * Sets or gets any cap on sales that might be in effect for this ticket when global stock
		 * mode is in effect.
		 *
		 * @param int $cap
		 *
		 * @return int
		 */
		public function global_stock_cap( $cap = null ) {
			if ( is_numeric( $cap ) ) {
				$this->global_stock_cap = (int) $cap;
			}

			return (int) $this->global_stock_cap;
		}

		/**
		 * Method to manage the protected `qty_sold` property of the Object
		 * Prevents setting `qty_sold` lower then zero
		 *
		 * @param int|null $value This will overwrite the old value
		 * @return int
		 */
		public function qty_sold( $value = null ) {
			return $this->qty_getter_setter( $this->qty_sold, $value );
		}

		/**
		 * Method to manage the protected `qty_pending` property of the Object
		 * Prevents setting `qty_pending` lower then zero
		 *
		 * @param int|null $value This will overwrite the old value
		 * @return int
		 */
		public function qty_pending( $value = null ) {
			return $this->qty_getter_setter( $this->qty_pending, $value );
		}

		/**
		 * Method to get/set protected quantity properties, disallowing illegal
		 * things such as setting a negative value.
		 *
		 * Callables are also supported, allowing properties to be lazily fetched
		 * or calculated on demand.
		 *
		 * @param int               &$property
		 * @param int|callable|null $value
		 *
		 * @return int|mixed
		 */
		protected function qty_getter_setter( &$property, $value = null ) {
			// Set to a positive numeric value
			if ( is_numeric( $value ) ) {
				$property = (int) $value;

				// Disallow negative values (and force to zero if one is passed)
				$property = max( (int) $property, 0 );
			}

			// Set to a callback
			if ( is_callable( $value ) ) {
				$property = $value;
			}

			// Return the callback's output if appropriate: but only when the
			// property is being set to avoid upfront costs
			if ( null === $value && is_callable( $property ) ) {
				return call_user_func( $property, $this->ID );
			}

			// Or else return the current property value
			return $property;
		}

		/**
		 * Magic getter to handle fetching protected properties
		 *
		 * @since 5.6.7 Add the `type` property to the list of properties that can be fetched.
		 *
		 * @param string $var Property to fetch.
		 *
		 * @return mixed Value of the property.
		 */
		public function __get( $var ) {
			switch ( $var ) {
				case 'stock':
					return $this->stock();
					break;
				case 'qty_pending':
					return $this->qty_pending();
					break;
				case 'qty_sold':
					return $this->qty_sold();
					break;
				case 'qty_refunded':
					return $this->qty_refunded();
					break;
				case 'qty_completed':
					return $this->qty_completed();
					break;
				case 'qty_cancelled':
					return $this->qty_cancelled();
					break;
				case 'type':
					return $this->type();
					break;
			}

			return null;
		}

		/**
		 * Magic setter to handle setting protected properties
		 *
		 * @since 5.6.7 Add the `type` property to the list of properties that can be set.
		 *
		 * @param string $var   Property to set.
		 * @param mixed  $value Value to set the property to.
		 */
		public function __set( $var, $value ) {
			switch ( $var ) {
				case 'stock':
					return $this->stock( $value );
					break;
				case 'qty_pending':
					return $this->qty_pending( $value );
					break;
				case 'qty_sold':
					return $this->qty_sold( $value );
					break;
				case 'qty_refunded':
					return $this->qty_refunded( $value );
					break;
				case 'qty_cancelled':
					return $this->qty_cancelled( $value );
					break;
				case 'type':
					if ( $this->ID ) {
						update_post_meta( $this->ID, '_type', $value );
					}
					break;
			}

			return null;
		}

		/**
		 * Method to manage the protected `qty_cancelled` property of the Object
		 * Prevents setting `qty_cancelled` lower then zero
		 *
		 * @param int|null $value This will overwrite the old value
		 * @return int
		 */
		public function qty_cancelled( $value = null ) {
			// If the Value was passed as numeric value overwrite
			if ( is_numeric( $value ) ) {
				$this->qty_cancelled = $value;
			}

			// Prevents qty_cancelled from going negative
			$this->qty_cancelled = max( (int) $this->qty_cancelled, 0 );

			// return the new Qty Cancelled
			return $this->qty_cancelled;
		}

		/**
		 * Method to manage the protected `qty_refunded` property of the Object
		 * Prevents setting `qty_refunded` lower then zero
		 *
		 * @since 4.7.3
		 *
		 * @param int|null $value This will overwrite the old value
		 * @return int
		 */
		public function qty_refunded( $value = null ) {
			// If the Value was passed as numeric value overwrite
			if ( is_numeric( $value ) ) {
				$this->qty_refunded = $value;
			}

			// Prevents qty_refunded from going negative
			$this->qty_refunded = max( (int) $this->qty_refunded, 0 );

			// return the new Qty Refunded
			return $this->qty_refunded;
		}

		/**
		 * Method to manage the protected `qty_completed` property of the Object
		 * Prevents setting `qty_completed` lower then zero
		 *
		 * @since 4.7.3
		 *
		 * @param int|null $value This will overwrite the old value
		 * @return int
		 */
		public function qty_completed( $value = null ) {
			// If the Value was passed as numeric value, overwrite.
			if ( is_numeric( $value ) ) {
				$this->qty_completed = $value;
			}

			// Prevents qty_completed from going negative.
			$this->qty_completed = max( (int) $this->qty_completed, 0 );

			// Return the new quantity completed.
			return $this->qty_completed;
		}

		/**
		 * Returns an instance of the provider class if found. If found, sets class property if not yet set.
		 *
		 * @since 4.1
		 * @since 4.12.3 Use new helper method to account for possibly inactive ticket provider. Set provider if found.
		 *
		 * @return Tribe__Tickets__Tickets|false Ticket provider instance or False if provider is not active.
		 */
		public function get_provider() {
			// Unexpected but we need to make sure we have something usable to start with.
			if (
				empty( $this->provider )
				&& empty( $this->provider_class )
			) {
				return false;
			}

			// If class provider if already set, we're done.
			if ( $this->provider instanceof Tribe__Tickets__Tickets ) {
				return $this->provider;
			}

			// Set class provider if valid, then return value even if invalid provider.
			$provider = Tribe__Tickets__Tickets::get_ticket_provider_instance( $this->provider_class );

			if ( ! empty( $provider ) ) {
				$this->provider = $provider;
			}

			return $provider;
		}

		/**
		 * Returns the ID of the event post this ticket belongs to.
		 *
		 * @return WP_Post|null
		 */
		public function get_event() {
			$provider = $this->get_provider();

			if ( ! empty( $provider ) ) {
				return $provider->get_event_for_ticket( $this->ID );
			}

			return null;
		}

		/**
		 * Returns whether the ticket description should show on
		 * the front page and in emails. Defaults to true.
		 *
		 * @since 4.6
		 *
		 * @return boolean
		 */
		public function show_description() {
			/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
			$tickets_handler = tribe( 'tickets.handler' );

			$key = $tickets_handler->key_show_description;

			$show = true;
			if ( metadata_exists( 'post', $this->ID, $key ) ) {
				$show = get_post_meta( $this->ID, $key, true );
			}

			/**
			 * Allows filtering of the value so we can for example, disable it for a theme/site
			 *
			 * @since 4.6
			 *
			 * @param boolean whether to show the description or not
			 * @param int ticket ID
			 */
			$show = apply_filters( 'tribe_tickets_show_description', $show, $this->ID );

			// Make sure we have the correct value
			return tribe_is_truthy( $show );
		}

		/**
		 * Access the ID of the Event parent of the current Ticket.
		 *
		 * @since 4.7.1
		 *
		 * @return int|null
		 */
		public function get_event_id() {
			if ( is_null( $this->event_id ) ) {
				$event = $this->get_event();
				if ( $event instanceof WP_Post ) {
					$this->event_id = $event->ID;
				}
			}
			return $this->event_id;
		}

		/**
		 * Check if the ticket has meta enabled.
		 *
		 * @since 5.0.3
		 *
		 * @return bool Whether the ticket has meta enabled.
		 */
		public function has_meta_enabled() {
			/**
			 * Filters if the ticket has meta or not.
			 *
			 * @param bool $has_meta Whether the ticket has meta enabled.
			 * @param int  $ticket_id The ticket ID.
			 */
			return (bool) apply_filters( 'tribe_tickets_has_meta_enabled', false, $this->ID );
		}

		/**
		 * Determine whether the ticket has ticket cache enabled.
		 *
		 * @since 5.1.0
		 *
		 * @return bool Whether the ticket has ticket cache enabled.
		 */
		public function is_ticket_cache_enabled() {
			/**
			 * Allow filtering whether the ticket has ticket cache enabled.
			 *
			 * @since 5.1.0
			 *
			 * @param bool $is_cache_enabled  Whether the ticket has ticket cache enabled.
			 * @param int  $ticket_id The ticket ID.
			 */
			return (bool) apply_filters( 'tribe_tickets_ticket_object_is_ticket_cache_enabled', true, $this->ID );
		}

		/**
		 * Dumps the Ticket Object into an array.
		 *
		 * @since 5.6.4
		 *
		 * @return array<string,mixed> The Ticket Object in array format.
		 */
		public function to_array(): array {
			return get_object_vars( $this );
		}

		/**
		 * Returns the ticket type.
		 *
		 * @since 5.6.7
		 *
		 * @return string The ticket type.
		 */
		public function type(): string {
			if ( ! $this->ID ) {
				return 'default';
			}

			if ( ! isset( $this->type ) ) {
				$this->type = get_post_meta( $this->ID, '_type', true ) ?: 'default';
			}

			return $this->type;
		}

		/**
		 * Updates the ticket type.
		 *
		 * Note: this method will NOT update the `_type` meta key used to store the Ticket type permanently, just
		 * the Ticket object `type` property.
		 *
		 * @since 5.6.7
		 *
		 * @param string $type The ticket type.
		 */
		public function setType( string $type = 'default' ): void {
			$this->type = $type;
		}
	}
}
