<?php
// phpcs:disable WordPress.WP.I18n.MissingTranslatorsComment

// Don't load directly
if (!defined('ABSPATH')) { exit; }

if (class_exists('R34ICSPro') && !class_exists('R34ICSProEvent')) {

	class R34ICSProEvent extends R34ICSPro {
		
		public $category = null;
		public $event = null;

		private $feed_uri_base = '/wp-json/r34icspro/v5/ics/';
		private $feed_uri_admin_base = '/wp-json/r34icspro/v5/ics_admin/';
		private $feed_uri_category_base = '/wp-json/r34icspro/v5/ics/';
		private $feed_uri_event_base = '/wp-json/r34icspro/v5/ics_event/';
		private $feed_uri_suffix = '';
		
		public function __construct() {
		
			// Bail out early if option is turned off
			if (!get_option('r34icspro_enable_event_cpt')) { return; }
	
			// Set base timezone for wp_date() functions in templates
			// try/catch is because this can cause a fatal error on a brand new installation in wp-env
			try { $this->tz = new DateTimeZone(get_option('timezone_string', 'UTC')); } catch (Exception $e) { }
	
			// Set property values
			// Remember: This method overrides parent::__construct()
			global $r34ics_embedded;
			if (!empty($r34ics_embedded)) {
				$this->r34ics_path = plugin_dir_path(dirname(__FILE__)) . 'vendors/ics-calendar';
			}
			elseif (empty($this->r34ics_path)) {
				$this->r34ics_path = WP_PLUGIN_DIR . '/ics-calendar';
			}
			$this->ical_path = $this->r34ics_path . $this->ical_path;
			$this->event_path = $this->r34ics_path . $this->event_path;
			$this->ical_legacy_path = $this->r34ics_path . $this->ical_legacy_path;
			$this->event_legacy_path = $this->r34ics_path . $this->event_legacy_path;
			$this->admin_sidebar_path = plugin_dir_path(dirname(__FILE__)) . 'templates/admin/pro_sidebar.php';
			$this->feed_uri_suffix = $this->_get_ics_uri_suffix();
		
			// Load ACF logic for ICS Event CPT
			add_action('acf/init', array(&$this, 'acf_init_cpt_event'), 3 - PHP_INT_MAX);
			
			// Modify admin index page
			add_action('admin_head', array(&$this, 'admin_index'));
			
			// Set up admin menu
			add_action('admin_menu', array(&$this, 'admin_event_menu'));
		
			// Register custom post type
			add_action('init', array(&$this, 'custom_post_type_event'));
						
			// Register REST API endpoint
			add_action('rest_api_init', array(&$this, 'rest_api_init'));
			
			// Customize category row actions in admin
			add_filter('r34icspro_event_category_row_actions', array(&$this, 'admin_category_row_actions'), 10, 2);
		
		}
		
		
		public function acf_init_cpt_event() {

			// Load our ACF field groups, only when needed	
			add_action('current_screen', function() {
				if (function_exists('get_current_screen') && $current_screen = get_current_screen()) {
					// ICS Events
					if ($current_screen->id == 'r34icspro_event') {
						include_once(plugin_dir_path(dirname(__FILE__)) . 'field_groups/field_groups_ics_events.php');
					}
				}
			});
			
			// Pass values dynamically from query string when adding new event
			add_filter('acf/load_field/name=start_date', array(&$this, 'acf_load_field_event_dates'));
			add_filter('acf/load_field/name=start_datetime', array(&$this, 'acf_load_field_event_dates'));
			add_filter('acf/load_field/name=end_date', array(&$this, 'acf_load_field_event_dates'));
			add_filter('acf/load_field/name=end_datetime', array(&$this, 'acf_load_field_event_dates'));
		
		}
		
		
		public function acf_load_field_event_dates($field) {
			if (empty($field['value'])) {
				// phpcs:disable WordPress.Security.NonceVerification.Recommended
				// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
				if (!empty($_GET['event_date']) && $event_date = sanitize_text_field($_GET['event_date'])) {
					$placeholder_time = strpos($field['name'], 'end') !== false ? '10:30:00' : '10:00:00';
					$field['value'] = r34ics_date('Y-m-d H:i:s', $event_date . ' ' . $placeholder_time);
				}
				// phpcs:enable WordPress.Security.NonceVerification.Recommended
				// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			}
			return $field;
		}


		// Run when an event is saved
		public function acf_save_post($post_id) {
			if (get_post_type($post_id) == 'r34icspro_event') {
				// Concatenate start/end dates for sorting
				if ($start_sortable = get_post_meta($post_id, 'start_datetime', true) ?: get_post_meta($post_id, 'start_date', true)) {
					update_post_meta($post_id, 'start_sortable', r34ics_date('YmdHis', $start_sortable));
				}
				if ($end_sortable = get_post_meta($post_id, 'end_datetime', true) ?: get_post_meta($post_id, 'end_date', true)) {
					update_post_meta($post_id, 'end_sortable', r34ics_date('YmdHis', $end_sortable));
				}
				if ($rrule_sortable = get_post_meta($post_id, 'rrule', true)) {
					update_post_meta($post_id, 'rrule_sortable', wp_strip_all_tags(r34ics_recurrence_description($this->_get_ics_rrule($post_id))));
				}
				// Erase all-day start/end on time-based events, and vice-versa
				/**
				 * Addresses an issue where ACF retains data for fields even if they are
				 * hidden by conditional logic.
				 */
				if (get_post_meta($post_id, 'all_day', true) == 1) {
					update_post_meta($post_id, 'start_datetime', get_post_meta($post_id, 'start_date', true));
					update_post_meta($post_id, 'end_datetime', get_post_meta($post_id, 'end_date', true));
				}
				else {
					update_post_meta($post_id, 'start_date', get_post_meta($post_id, 'start_datetime', true));
					update_post_meta($post_id, 'end_date', get_post_meta($post_id, 'end_datetime', true));
				}
			}
		}
		
		
		public function admin_calendar_view() {
			global $r34icspro_admin_nonce;
			$r34icspro_admin_nonce = wp_create_nonce('r34icspro_admin_nonce');
			/**
			 * Note: This is all happening server-side, so we can't rely on the regular nonce
			 * functions. Instead we write this nonce to a transient so we can check it later
			 * ourselves as *part* of the verification process. We also verify that the request
			 * is coming from the server itself. (NOT the client IP address!)
			 *
			 * We are saving the client IP address as the value for possible future use; the
			 * current logic disregards the value of the transient, just checks that it exists.
			 */
			
			// phpcs:disable WordPress.Security.NonceVerification.Recommended
			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			
			set_transient('r34icspro_admin_nonce_' . $r34icspro_admin_nonce, $_SERVER['REMOTE_ADDR'], DAY_IN_SECONDS);
			
			// @todo Determine why we need to manually load these instead of enqueuing
			// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
			// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
			?>
			<link rel="stylesheet" href="<?php echo esc_url(plugin_dir_url(dirname(__FILE__))); ?>vendors/ics-calendar/assets/style.min.css" />
			<link rel="stylesheet" href="<?php echo esc_url(plugin_dir_url(dirname(__FILE__))); ?>assets/style-pro.min.css" />
			<script defer src="<?php echo esc_url(plugin_dir_url(dirname(__FILE__))); ?>vendors/ics-calendar/assets/script.min.js"></script>
			<script defer src="<?php echo esc_url(plugin_dir_url(dirname(__FILE__))); ?>assets/script-pro.min.js"></script>
			<script defer>
				var ics_calendar_i18n = <?php echo wp_json_encode(array(
					'hide_past_events' => esc_html__('Hide past events', 'ics-calendar-pro'),
					'show_past_events' => esc_html__('Show past events', 'ics-calendar-pro'),
				)); ?>;
				var r34ics_days_of_week_map = <?php echo wp_json_encode((array)$this->days_of_week_map()); ?>;
			</script>
			<?php
			// phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
			// phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
			
			// Modify the contents of the ICS data
			add_filter('r34ics_display_calendar_event_item', array(&$this, 'admin_calendar_view_event_filter'), 10, 3);
			
			// What month are we looking at?
			$r34icsym = substr(intval($_GET['r34icsym'] ?? wp_date('Ym')), 0, 6);
			$startdate = $r34icsym . '01';
			
			// Are we filtering by category?
			$categories = get_terms(array(
				'post_type' => 'r34icspro_event',
				'taxonomy' => 'r34icspro_event_category',
			));
			if ($r34icspro_event_category = sanitize_text_field(wp_unslash($_GET['r34icspro_event_category'] ?? ''))) {
				$feed_url = home_url($this->get_feed_uri_base('admin') . '/' . $r34icspro_admin_nonce . '/' . $r34icspro_event_category);
				$public_category_feed_url = home_url($this->get_feed_uri_base('category') . '/' . $r34icspro_event_category);
			}
			else {
				$feed_url = home_url($this->get_feed_uri_base('admin') . '/' . $r34icspro_admin_nonce);
			}
			$public_feed_url = home_url($this->get_feed_uri_base());
			// phpcs:enable WordPress.Security.NonceVerification.Recommended
			// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			
			// Verify that we are able to access the feed URL
			$response = wp_remote_get($feed_url ?: '', array('limit_response_size' => 0));
			$response_code = wp_remote_retrieve_response_code($response);
			$response_message = wp_remote_retrieve_response_message($response);
			if ($response_code >= 400) {
				global $r34ics_deferred_admin_notices;
				$r34ics_deferred_admin_notices['r34icspro_ics_events_admin_calendar_feed_url_error'] = array(
					'content' => '<p>' . sprintf(esc_html__('Unable to retrieve calendar event data. If this problem persists, please %1$scontact customer support%2$s and provide this status code: %3$s', 'ics-calendar-pro'), '<a href="https://icscalendar.com/support/" target="_blank">', '</a>', '<code>HTTP ' . intval($response_code) . ' ' . esc_attr($response_message) . '</code>') . '</p>',
					'status' => 'error',
					'dismissible' => false,
				);
			}
			
			// Render template
			include_once(plugin_dir_path(dirname(__FILE__)) . 'templates/admin/ics_events_calendar_view.php');
		}
		
		
		public function admin_calendar_view_event_filter($event_item, $event, $args) {
			if (is_admin()) {
				// Set placeholder title if event is missing one
				if (empty($event_item['label'])) { $event_item['label'] = '[...]'; }
				// Set event URL to editing screen
				if (!empty($event_item['x_r34ics_event_id'])) {
					$event_item['url'] = admin_url('post.php?post=' . intval($event_item['x_r34ics_event_id']) . '&action=edit');
				}
			}
			return $event_item;
		}
		
		
		public function admin_category_row_actions($actions, $tag) {
			$actions['events'] = '<a href="' . admin_url('edit.php?post_type=r34icspro_event&page=ics-events-calendar-view&r34icspro_event_category=' . $tag->slug) . '" style="white-space: nowrap;">' . __('Events', 'ics-calendar-pro') . '</a>';
			return $actions;
		}
		
		
		public function admin_event_menu() {
			if (get_option('r34icspro_enable_event_cpt')) {
				// Custom calendar view for admin index
				add_submenu_page(
					'edit.php?post_type=r34icspro_event',
					__('ICS Events', 'ics-calendar-pro'),
					__('All Events', 'ics-calendar-pro'),
					get_option('r34ics_admin_access', 'edit_others_posts'),
					'ics-events-calendar-view',
					array(&$this, 'admin_calendar_view'),
					1 - PHP_INT_MAX
				);
				// Remove default admin index
				remove_submenu_page(
					'edit.php?post_type=r34icspro_event',
					'edit.php?post_type=r34icspro_event'
				);
			}
			if (get_option('r34icspro_enable_event_cpt_archive')) {
				// Front-end calendar view
				add_submenu_page(
					'edit.php?post_type=r34icspro_event',
					__('View Calendar', 'ics-calendar-pro'),
					__('View Calendar', 'ics-calendar-pro'),
					get_option('r34ics_admin_access', 'edit_others_posts'),
					home_url('/' . get_option('r34icspro_event_cpt_slug') . '/'),
					'',
					99
				);
			}
		}
		
		
		public function admin_index() {
			$current_screen = get_current_screen();
			if (@$current_screen->base == 'edit' && @$current_screen->post_type == 'r34icspro_event') {
				?>
				<script>
				jQuery(function() {
					jQuery("body.post-type-r34icspro_event .wrap hr.wp-header-end").before('<a href="<?php echo esc_url(admin_url('edit.php?post_type=r34icspro_event&page=ics-events-calendar-view')); ?>" class="page-title-action"><?php esc_html_e('Switch to Calendar View', 'ics-calendar-pro'); ?></a>');
				});
				</script>
				<?php
			}
		}


		public function custom_post_type_event() {
			
			// Is the CPT enabled?
			if (!get_option('r34icspro_enable_event_cpt')) { return false; }
			
			// Custom capability for accessing this CPT
			$cap = get_option('r34ics_admin_access', 'edit_others_posts');
		
			register_post_type('r34icspro_event', array(
				'labels' =>	array(
					'name'               => __('ICS Events', 'ics-calendar-pro'),
					'singular_name'      => __('ICS Event', 'ics-calendar-pro'),
					'add_new_item'       => __('Add New Event', 'ics-calendar-pro'),
					'add_new'            => __('Add New Event', 'ics-calendar-pro'),
					'all_items'          => __('All Events', 'ics-calendar-pro'),
					'edit_item'          => __('Edit Event', 'ics-calendar-pro'),
					'menu_name'          => __('ICS Events', 'ics-calendar-pro'),
					'name_admin_bar'     => __('ICS Event', 'ics-calendar-pro'),
					'new_item'           => __('New ICS Event', 'ics-calendar-pro'),
					'not_found_in_trash' => __('No ICS events found in Trash.', 'ics-calendar-pro'),
					'not_found'          => __('No ICS events found.', 'ics-calendar-pro'),
					'parent_item_colon'  => false,
					'search_items'       => __('Search ICS Events', 'ics-calendar-pro'),
					'view_item'          => __('View ICS Event', 'ics-calendar-pro'),
				),
				'capability_type' => 'post',
				'capabilities' => array(
					'delete_post' => $cap,
					'delete_posts' => $cap,
					'edit_post' => $cap,
					'edit_posts' => $cap,
					'read_post' => $cap, // This is so Permalink will actually fill in the href on the admin screen! ¯\_(ツ)_/¯
				),
				'description' => __('Events to be included in the built-in ICS Calendar.', 'ics-calendar-pro'),
				'has_archive' => (bool)get_option('r34icspro_enable_event_cpt_archive'), // Front end requires this to be cast as a boolean! ¯\_(ツ)_/¯
				'hierarchical' => false,
				'menu_icon' => $this->icon_logo,
				'menu_position' => 49.2,
				'public' => get_option('r34icspro_enable_event_cpt_single'),
				'query_var' => 'r34icspro_event',
				'rewrite' => array('slug' => get_option('r34icspro_event_cpt_slug', 'ics-events')),
				'show_in_nav_menus' =>	get_option('r34icspro_enable_event_cpt_single'),
				'show_in_rest' => true,
				'show_ui' => true,
				'supports' => array('title', 'thumbnail', 'revisions', 'author', 'excerpt'),
				'taxonomies' => array('r34icspro_event_category', 'r34icspro_event_location', 'r34icspro_event_organizer'),
			));
			add_theme_support('post-thumbnails', array('r34icspro_event'));
	
			register_taxonomy('r34icspro_event_category', 'r34icspro_event', array(
				'labels' => array(
					'name'                       => __('Event Categories', 'ics-calendar-pro'),
					'singular_name'              => __('Category', 'ics-calendar-pro'),
					'add_new_item'               => __('Add New Category', 'ics-calendar-pro'),
					'add_or_remove_items'        => __('Add or remove categories', 'ics-calendar-pro'),
					'all_items'                  => __('All categories', 'ics-calendar-pro'),
					'choose_from_most_used'      => __('Choose from the most used categories', 'ics-calendar-pro'),
					'desc_field_description'     => __('The description for the category. May be publicly viewable when the category calendar is shared.', 'ics-calendar-pro'),
					'edit_item'                  => __('Edit Category', 'ics-calendar-pro'),
					'menu_name'                  => __('Categories', 'ics-calendar-pro'),
					'name_field_description'     => __('The name for the category. May be publicly viewable when the category calendar is shared.', 'ics-calendar-pro'),
					'new_item_name'              => __('New Category Name', 'ics-calendar-pro'),
					'not_found'                  => __('No categories found.', 'ics-calendar-pro'),
					'popular_items'              => __('Popular Categories', 'ics-calendar-pro'),
					'search_items'               => __('Search Categories', 'ics-calendar-pro'),
					'separate_items_with_commas' => __('Separate categories with commas', 'ics-calendar-pro'),
					'slug_field_description'     => __('The slug for the category. Will be part of the URL for the category ICS feed.', 'ics-calendar-pro'),
					'update_item'                => __('Update Category', 'ics-calendar-pro'),
				),
				'capabilities' => array(
					'manage_terms' => $cap,
					'edit_terms' => $cap,
					'delete_terms' => $cap,
					'assign_terms' => $cap,
				),
				'has_archive' => get_option('r34icspro_enable_event_cpt_archive'),
				'hierarchical' => false,
				'public' => get_option('r34icspro_enable_event_cpt_archive'),
				'query_var' => 'r34icspro_event_category',
				'rewrite' =>	array('slug' => 'ics-categories', 'with_front' => false),
				'show_admin_column' => true,
				'show_in_nav_menus' =>	true,
				//'show_in_rest' => true, // adds Block Editor support
				'show_in_nav_menus' => get_option('r34icspro_enable_event_cpt_archive'),
				'show_ui' =>	true,
				'update_count_callback' => '_update_post_term_count',
			));
				
			register_taxonomy('r34icspro_event_location', 'r34icspro_event', array(
				'labels' => array(
					'name'                       => __('Event Locations', 'ics-calendar-pro'),
					'singular_name'              => __('Location', 'ics-calendar-pro'),
					'add_new_item'               => __('Add New Location', 'ics-calendar-pro'),
					'add_or_remove_items'        => __('Add or remove locations', 'ics-calendar-pro'),
					'all_items'                  => __('All Locations', 'ics-calendar-pro'),
					'choose_from_most_used'      => __('Choose from the most used locations', 'ics-calendar-pro'),
					'desc_field_description'     => __('If entered, will be used as the location text in the feed for all events assigned to this location.', 'ics-calendar-pro'),
					'edit_item'                  => __('Edit Location', 'ics-calendar-pro'),
					'menu_name'                  => __('Locations', 'ics-calendar-pro'),
					'name_field_description'     => sprintf(esc_html__('Will be used as the location text in the feed for all events assigned to this location, %1$sif Description is left blank.%2$s', 'ics-calendar-pro'), '<em>', '</em>'),
					'new_item_name'              => __('New Location Name', 'ics-calendar-pro'),
					'not_found'                  => __('No locations found.', 'ics-calendar-pro'),
					'popular_items'              => __('Popular Locations', 'ics-calendar-pro'),
					'search_items'               => __('Search Locations', 'ics-calendar-pro'),
					'separate_items_with_commas' => __('Separate locations with commas', 'ics-calendar-pro'),
					'slug_field_description'     => '',
					'update_item'                => __('Update Location', 'ics-calendar-pro'),
				),
				'capabilities' => array(
					'manage_terms' => $cap,
					'edit_terms' => $cap,
					'delete_terms' => $cap,
					'assign_terms' => $cap,
				),
				'has_archive' => false,
				'hierarchical' => false,
				'public' => false,
				'query_var' => 'r34icspro_event_location',
				'rewrite' => false,
				'show_admin_column' => true,
				//'show_in_rest' => true, // adds Block Editor support
				'show_in_nav_menus' => false,
				'show_ui' =>	true,
				'update_count_callback' => '_update_post_term_count',
			));
				
			register_taxonomy('r34icspro_event_organizer', 'r34icspro_event', array(
				'labels' => array(
					'name'                       => __('Event Organizers', 'ics-calendar-pro'),
					'singular_name'              => __('Organizer', 'ics-calendar-pro'),
					'add_new_item'               => __('Add New Organizer', 'ics-calendar-pro'),
					'add_or_remove_items'        => __('Add or remove organizers', 'ics-calendar-pro'),
					'all_items'                  => __('All Organizers', 'ics-calendar-pro'),
					'choose_from_most_used'      => __('Choose from the most used organizers', 'ics-calendar-pro'),
					'desc_field_description'     => __('If entered, will be used as the organizer text in the feed for all events assigned to this organizer.', 'ics-calendar-pro'),
					'edit_item'                  => __('Edit Organizer', 'ics-calendar-pro'),
					'menu_name'                  => __('Organizers', 'ics-calendar-pro'),
					'name_field_description'     => sprintf(esc_html__('Will be used as the organizer text in the feed for all events assigned to this organizer, %1$sif Description is left blank.%2$s', 'ics-calendar-pro'), '<em>', '</em>'),
					'new_item_name'              => __('New Organizer Name', 'ics-calendar-pro'),
					'not_found'                  => __('No organizers found.', 'ics-calendar-pro'),
					'popular_items'              => __('Popular Organizers', 'ics-calendar-pro'),
					'search_items'               => __('Search Organizers', 'ics-calendar-pro'),
					'separate_items_with_commas' => __('Separate organizers with commas', 'ics-calendar-pro'),
					'slug_field_description'     => '',
					'update_item'                => __('Update Organizer', 'ics-calendar-pro'),
				),
				'capabilities' => array(
					'manage_terms' => $cap,
					'edit_terms' => $cap,
					'delete_terms' => $cap,
					'assign_terms' => $cap,
				),
				'has_archive' => false,
				'hierarchical' => false,
				'public' => false,
				'query_var' => 'r34icspro_event_organizer',
				'rewrite' => false,
				'show_admin_column' => true,
				//'show_in_rest' => true, // adds Block Editor support
				'show_in_nav_menus' => false,
				'show_ui' =>	true,
				'update_count_callback' => '_update_post_term_count',
			));
				
			// Admin columns
			add_filter('manage_r34icspro_event_posts_columns', function($columns) {
				// Add start and end dates, and recurrence status
				$columns = array_merge(array_slice($columns, 0, 2), array(
					'start' => esc_html__('Start', 'ics-calendar-pro'),
					'end' => esc_html__('End', 'ics-calendar-pro'),
					'recurrence' => esc_html__('Recurrence', 'ics-calendar-pro'),
				), array_slice($columns, 2));
				// Remove 'Event ' prefix from column labels
				foreach ((array)$columns as $column => $label) {
					if (strpos($label, 'Event ') === 0) {
						$columns[$column] = str_replace('Event ', '', $label);
					}
				}
				// Change heading for publish date column
				$columns['date'] = esc_html__('Modified', 'ics-calendar-pro');
				return $columns;
			});
			add_action('manage_r34icspro_event_posts_custom_column' , function($column, $post_id) {
				switch ($column) {
					case 'start':
						if (get_post_meta($post_id, 'all_day', true) && $start_date = get_post_meta($post_id, 'start_date', true)) {
							echo wp_kses_post(r34ics_date(get_option('date_format'), $start_date));
						}
						elseif ($start_datetime = get_post_meta($post_id, 'start_datetime', true)) {
							echo wp_kses_post(r34ics_date(get_option('date_format') . ' ' . get_option('time_format'), $start_datetime));
						}
						else {
							echo '&#8212;';
						}
						break;
					case 'end':
						if (get_post_meta($post_id, 'all_day', true) && $end_date = get_post_meta($post_id, 'end_date', true)) {
							echo wp_kses_post(r34ics_date(get_option('date_format'), $end_date));
						}
						elseif ($end_datetime = get_post_meta($post_id, 'end_datetime', true)) {
							echo wp_kses_post(r34ics_date(get_option('date_format') . ' ' . get_option('time_format'), $end_datetime));
						}
						else {
							echo '&#8212;';
						}
						break;
					case 'recurrence':
						if (get_post_meta($post_id, 'rrule', true)) {
							echo wp_kses_post(r34ics_recurrence_description($this->_get_ics_rrule($post_id)));
						}
						else {
							echo '&#8212;';
						}
						break;
					default: break;
				}
			}, 10, 2);
			add_filter('manage_edit-r34icspro_event_sortable_columns', function($columns) {
				$columns['start'] = 'start_sortable';
				$columns['end'] = 'end_sortable';
				$columns['recurrence'] = 'rrule_sortable';
				return $columns;
			});
			add_action('pre_get_posts', function($query) {
				if (!is_admin()) { return; }
				if ($query->get('post_type') == 'r34icspro_event') {
					$orderby = $query->get('orderby');
					if ($orderby == 'start_sortable' || $orderby == 'end_sortable' || $orderby == 'rrule_sortable') {
						$meta_query = array(
							'relation' => 'OR',
							array('key' => $orderby, 'compare' => 'NOT EXISTS'),
							array('key' => $orderby)
						);
						$query->set('meta_query', $meta_query);
						$query->set('orderby', 'meta_value');
					}
				}
			});
			
			// Admin filters
			add_action('restrict_manage_posts', function($post_type) {
				if ($post_type != 'r34icspro_event') { return; }
				// ICS Event taxonomies
				$taxonomies = array('r34icspro_event_category', 'r34icspro_event_location', 'r34icspro_event_organizer');
				foreach ($taxonomies as $slug) {
					$taxonomy = get_taxonomy($slug);
					// phpcs:disable WordPress.Security.NonceVerification.Recommended
					$selected = isset($_REQUEST[$slug]) ? sanitize_title(wp_unslash($_REQUEST[$slug])) : '';
					// phpcs:enable WordPress.Security.NonceVerification.Recommended
					wp_dropdown_categories(array(
						'hide_if_empty' => true,
						'hierarchical' => true,
						'name' => $slug,
						'orderby' => 'name',
						'selected' => $selected,
						'show_option_all' => $taxonomy->labels->all_items,
						'taxonomy' => $slug,
						'value_field' => 'slug',
					));
				}
			}, 10, 1);

			// Remove default WordPress publish date dropdown; useful but confusing
			add_filter('months_dropdown_results', function($months) {
				global $typenow;
				if ($typenow == 'r34icspro_event') { return array(); }
				return $months;
			});
			
			// Meta boxes for editing screens
			add_action('add_meta_boxes', function() {
				global $post;
				
				// Support sidebar
				add_meta_box(
					'r34icspro-support',
					'ICS Calendar Pro', // We don't want to translate this
					array(&$this, 'admin_support_sidebar'),
					'r34icspro_event',
					'side',
					'low'
				);
			});
			
			// Custom templates
			add_filter('archive_template', function($archive_template) {
				if (is_post_type_archive('r34icspro_event')) {
					if (get_option('r34icspro_enable_event_cpt_archive')) {
						$archive_template = locate_template('ics-calendar/archive-r34icspro_event.php') ?: plugin_dir_path(dirname(__FILE__)) . 'templates/archive-r34icspro_event.php';
					}
					else {
						wp_safe_redirect(home_url('/')); exit;
					}
				}
				return $archive_template;
			});
			add_filter('single_template', function($single_template) {
				global $post;
				if (get_option('r34icspro_enable_event_cpt_single') && $post->post_type == 'r34icspro_event') {
					$single_template = locate_template('ics-calendar/single-r34icspro_event.php') ?: plugin_dir_path(dirname(__FILE__)) . 'templates/single-r34icspro_event.php';
				}
				return $single_template;
			});
			add_filter('taxonomy_template', function($taxonomy_template) {
				if (get_option('r34icspro_enable_event_cpt_archive') && is_tax('r34icspro_event_category')) {
					$taxonomy_template = locate_template('ics-calendar/archive-r34icspro_event.php') ?: plugin_dir_path(dirname(__FILE__)) . 'templates/archive-r34icspro_event.php';
				}
				return $taxonomy_template;
			});
			
			// CPT-related actions
			add_action('acf/save_post', array(&$this, 'acf_save_post'));
			add_action('save_post_r34icspro_event', array(&$this, 'acf_save_post'));
			add_action('save_post_r34icspro_event', 'r34ics_purge_calendar_transients', PHP_INT_MAX - 1);
			
		}
		
		
		// ICS Event CPT: Get custom fields
		// A simplified way of retrieving ACF-saved fields without ACF active on the front end
		// Note: All data should be sanitized with wp_kses_post(), esc_attr(), or similar for output!
		public function get_custom_fields($post_id) {
			// We've already retrieved this event's fields
			if (is_array($this->event) && $this->event['ID'] == $post_id) { return $this->event['fields']; }
			// Get this event's fields
			$this->event = array(
				'ID' => $post_id,
				'fields' => array(),
			);
			$allowed_fields = array(
				'all_day',
				'start_date',
				'start_datetime',
				'end_date',
				'end_datetime',
				'tzid',
				'rrule',
				'rrule_interval',
				'rrule_freq',
				'rrule_week_days',
				'rrule_month_by',
				'rrule_month_exact_date',
				'rrule_month_week',
				'rrule_month_week_days',
				'rrule_end',
				'rrule_end_date',
				'rrule_count',
				'rrule_exdates',
				'rrule_exdate',
				'description',
				'event_url',
				'event_url_append_qs',
				'contact',
				'attach_0_file',
				'attach_1_file',
				'attach_2_file',
				'attach_3_file',
				'attach_4_file',
				'attach_5_file',
				'attach_6_file',
				'attach_7_file',
				'attach_8_file',
				'attach_9_file',
				'color',
				'freebusy',
				'status',
				'class',
			);
			if ($fields = get_post_custom(intval($post_id))) {
				foreach ((array)$fields as $key => $value) {
					// Discard "hidden" fields (starts with underscore)
					if (!in_array($key, $allowed_fields)) { unset($fields[$key]); continue; }
					// Collapse single-node arrays
					if (is_array($value) && count($value) == 1) {
						$fields[$key] = implode('', $value);
					}
				}
				$this->event['fields'] = $fields;
				return $this->event['fields'];
			}
			return false;
		}
		
		
		// Get full ICS feed output for all events, one category, or a single event
		public function get_feed($category=null, $post_id=null, $include_drafts=false) {
			// Retrieve the events
			$args = array(
				'post_type' => 'r34icspro_event',
				'posts_per_page' => -1,
			);
			if ($include_drafts) {
				$args['post_status'] = 'any';
			}
			if (!empty($category)) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
				$args['tax_query'] = array(array(
					'taxonomy' => 'r34icspro_event_category',
					'field' => 'slug',
					'terms' => array($category),
				));
			}
			elseif (!empty($post_id)) {
				$args['p'] = intval($post_id);
			}
			$events = get_posts($args);
			$timezones = array(get_option('timezone_string'));
			// Set some basic parameters
			if (!empty($category)) {
				$category_term = get_term_by('slug', $category, 'r34icspro_event_category');
			}
			$prodid = '-//Room 34 Creative Services, LLC//ICS Calendar Pro ' . get_option('r34icspro_version') . '//' . strtoupper(substr(get_locale(),0,2));
			$calname = $category_term->name ?? get_bloginfo('name') . ' ' . esc_html__('Calendar', 'ics-calendar-pro');
			$caldesc = $category_term->description ?? '';
			
			// Build array of output lines
			$content = array();
			$content[] = 'BEGIN:VCALENDAR';
			$content[] = 'PRODID:' . r34ics_maybe_enfold($prodid, 7);
			$content[] = 'VERSION:2.0';
			$content[] = 'CALSCALE:GREGORIAN';
			$content[] = 'METHOD:PUBLISH';
			if (!empty($calname)) {
				$content[] = 'X-WR-CALNAME:' . r34ics_maybe_enfold($calname, 13);
			}
			$content[] = 'X-WR-TIMEZONE:' . get_option('timezone_string');
			if (!empty($caldesc)) {
				$content[] = 'X-WR-CALDESC:' . r34ics_maybe_enfold($caldesc, 13);
			}
			
			// Loop through events
			foreach ((array)$events as $post) {
				$tzid = $this->_get_ics_field($post->ID, 'tzid');
				if (!in_array($tzid, $timezones)) { $timezones[] = $tzid; }
				$content[] = 'BEGIN:VEVENT';
				$content[] = 'DTSTART;' . $this->_get_ics_datetime($post->ID, 'start');
				if ($dtend = $this->_get_ics_datetime($post->ID, 'end')) {
					$content[] = 'DTEND;' . $dtend;
				}
				if ($rrule = $this->_get_ics_rrule($post->ID)) {
					$content[] = 'RRULE:' . r34ics_maybe_enfold($rrule, 6);
					//$content[] = 'RECURRENCE-ID;' . $this->_get_ics_datetime($post->ID, 'start');
				}
				if ($exdate = $this->_get_ics_exdate($post->ID)) {
					$content[] = 'EXDATE;' . r34ics_maybe_enfold($exdate, 7);
				}
				$content[] = 'DTSTAMP:' . r34ics_date('Ymd\THis\Z');
				$content[] = 'UID:' . $this->_get_ics_uid($post->ID);
				$content[] = 'X-R34ICS-EVENT-ID:' . intval($post->ID);
				$content[] = 'CREATED:' . r34ics_date('Ymd\THis\Z', get_the_date('c', $post->ID));
				$content[] = 'LAST-MODIFIED:' . r34ics_date('Ymd\THis\Z', get_the_modified_date('c', $post->ID));
				if ($summary = get_the_title($post->ID)) {
					$post_status = get_post_status($post);
					if ($post_status != 'publish') {
						$summary .= ' [' . get_post_status_object($post_status)->label . ']';
					}
					$content[] = 'SUMMARY:' . r34ics_maybe_enfold($summary, 8);
				}
				$content[] = 'DESCRIPTION:' . r34ics_maybe_enfold($this->_get_ics_field($post->ID, 'description', false, true), 12);
				if (has_post_thumbnail($post->ID)) {
					$image_id = get_post_thumbnail_id($post->ID);
					$image_mime_type = get_post_mime_type($image_id) ?: '';
					$image_url = wp_get_attachment_url($image_id) ?: '';
					$content[] = 'ATTACH;' . r34ics_maybe_enfold('FMTTYPE=' . $image_mime_type . ':' . $image_url, 7);
					$content[] = 'IMAGE;VALUE=URI;' . r34ics_maybe_enfold('FMTTYPE=' . $image_mime_type . ':' . $image_url, 7);
				}
				for ($i = 0; $i <= 9; $i++) {
					if ($file_id = $this->_get_ics_field($post->ID, 'attach_' . $i . '_file')) {
						$file_mime_type = get_post_mime_type($file_id) ?: '';
						$file_url = wp_get_attachment_url($file_id) ?: '';
						$content[] = 'ATTACH;' . r34ics_maybe_enfold('FMTTYPE=' . $file_mime_type . ':' . $file_url, 7);
					}
				}
				if ($class = $this->_get_ics_field($post->ID, 'class', true)) {
					$content[] = 'CLASS:' . r34ics_maybe_enfold($class, 6);
				}
				if ($color = $this->_get_ics_field($post->ID, 'color')) {
					$content[] = 'COLOR:' . r34ics_maybe_enfold($color, 6);
				}
				if ($contact = $this->_get_ics_field($post->ID, 'contact')) {
					$content[] = 'CONTACT:' . r34ics_maybe_enfold($contact, 8);
				}
				if ($event_url = $this->_get_ics_field($post->ID, 'event_url')) {
					if ($event_url_append_qs = $this->_get_ics_field($post->ID, 'event_url_append_qs')) {
						$qs_delim = strpos($event_url, '?') === false ? '?' : '&';
						switch ($event_url_append_qs) {
							case 'details':
								$start = ($this->_get_ics_field($post->ID, 'all_day')
									? r34ics_date(get_option('date_format'), $this->_get_ics_field($post->ID, 'start_date'))
									: r34ics_date(get_option('date_format') . ' ' . get_option('time_format'), $this->_get_ics_field($post->ID, 'start_datetime')));
								$event_url .= $qs_delim . 'icsevent=' . rawurlencode(get_the_title($post->ID) . ' | ' . $start);
								break;
							case 'id':
								$event_url .= $qs_delim . 'icsevent=' . intval($post->ID);
								if (!empty($rrule)) {
									$start = ($this->_get_ics_field($post->ID, 'all_day')
										? $this->_get_ics_field($post->ID, 'start_date')
										: $this->_get_ics_field($post->ID, 'start_datetime'));
									$event_url .= rawurlencode('|' . $start);
								}
								break;
							default: break; // Do nothing
						}
					}
					$content[] = 'URL:' . r34ics_maybe_enfold($event_url, 4);
				}
				elseif (get_option('r34icspro_enable_event_cpt_single')) {
					$content[] = 'URL:' . r34ics_maybe_enfold(get_permalink($post->ID), 4);
				}
				if ($categories = $this->_get_ics_taxonomy($post->ID, 'r34icspro_event_category', true)) {
					$content[] = 'CATEGORIES:' . r34ics_maybe_enfold($categories, 11);
				}
				if ($location = $this->_get_ics_taxonomy($post->ID, 'r34icspro_event_location')) {
					$content[] = 'LOCATION:' . r34ics_maybe_enfold($location, 9);
				}
				if ($organizer = $this->_get_ics_taxonomy($post->ID, 'r34icspro_event_organizer')) {
					$content[] = 'ORGANIZER:' . r34ics_maybe_enfold($organizer, 10);
				}
				//$content[] = 'GEO:';
				//$content[] = 'RESOURCES:';
				//$content[] = 'SEQUENCE:1';
				/* @todo This seems to break Google Calendar imports... is it being used incorrectly?
				if ($freebusy = $this->_get_ics_field($post->ID, 'freebusy', true)) {
					$content[] = 'FREEBUSY:' . r34ics_maybe_enfold($freebusy, 9);
				}
				*/
				if ($status = $this->_get_ics_field($post->ID, 'status', true)) {
					$content[] = 'STATUS:' . r34ics_maybe_enfold($status, 7);
				}
				//$content[] = 'TRANSP:OPAQUE';
				$content[] = 'END:VEVENT';
			}
			
			// Append all timezones used (has to come last because we build the array as we go)
			foreach ((array)$timezones as $tzid) {
				$content = array_merge($content, r34ics_get_vtimezone($tzid));
			}
			
			// End output array
			$content[] = 'END:VCALENDAR';
			$content = implode("\r\n", $content);
			
			// Send output to browser
			header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
			header('Expires: 0');
			header('Content-Type: text/calendar');
			header('Content-Disposition: attachment; filename="' . sanitize_title($category ?? esc_attr__('calendar', 'ics-calendar-pro')) . '.ics"');
			header('Content-Length: ' . strlen($content ?: ''));
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $content;
			exit;
		}
		
		
		// Return feed URI base values
		public function get_feed_uri_base($type='') {
			switch ($type) {
				case 'admin':			return $this->feed_uri_admin_base . $this->feed_uri_suffix;			break;
				case 'category':	return $this->feed_uri_category_base . $this->feed_uri_suffix;	break;
				case 'event':			return $this->feed_uri_event_base . $this->feed_uri_suffix;			break;
				default:					return $this->feed_uri_base . $this->feed_uri_suffix;						break;
			}
			return '';
		}
		
		
		// Get a parsed event from the feed, including next and upcoming recurrences
		public function get_parsed_event($post_id) {
		
			// Make sure the post exists and is published
			if (get_post_status($post_id) != 'publish') { return false; }
			
			// get the post's feed URL and initialize iCal object
			$url = home_url($this->get_feed_uri_base('event') . '/' . $post_id);
			$recurring = !empty($this->_get_ics_field($post_id, 'rrule'));
			if (!$this->parser_loaded) { $this->_load_parser(!empty($args['legacyparser'])); }
			$ICal = new R34ICS_ICal\ICal('ICal.ics', array(
				'defaultSpan' => intval(ceil(($args['limitdays'] ?: 365) / 365)),
				'defaultTimeZone' => get_option('timezone_string'),
				'disableCharacterReplacement' => true,
				'filterDaysAfter' => ($recurring ? 366 : null),
				'filterDaysBefore' => ($recurring ? 0 : null),
				'skipRecurrence' => false,
			));
			$ICal->initUrl($url);
			$events = $ICal->events(); // Don't use eventsFromRange()! We want this event whenever it occurs/-ed
			if (!empty($events[0])) {
				$event = $events[0];
				$args = $this->shortcode_defaults_merge();
				extract($args);
				$url_tz = wp_timezone();
				$feed_key = 0;

				// @todo The following is copied from R34ICS::display_calendar(); encapsulate in a method

				// Set timezone(s) for event
				if (!empty($args['eventlocaltime']) || strpos('T', $args['timeformat']) !== false) {
					$event_start_tz = (!empty($event->dtstart_array[0]['TZID']) ? @timezone_open($event->dtstart_array[0]['TZID']) : $url_tz);
					$event_end_tz = (!empty($event->dtend_array[0]['TZID']) ? @timezone_open($event->dtend_array[0]['TZID']) : $url_tz);
				}
				else {
					$event_start_tz = $event_end_tz = $url_tz;
				}
			
				// Set start and end dates for event
				$dtstart_date = wp_date('Ymd', $event->dtstart_array[2], $event_start_tz);
				// Conditional is for events that are missing DTEND altogether, which should never be the case but has been observed in customer support
				$dtend_date = wp_date('Ymd', ($event->dtend_array[2] ?: $event->dtstart_array[2]), $event_end_tz);

				// All-day events
				if (strlen($event->dtstart ?: '') == 8 || (strpos(($event->dtstart ?: ''), 'T000000') !== false && strpos(($event->dtend ?: ''), 'T000000') !== false)) {
					$dtstart_time = '';
					$dtend_time = '';
					$all_day = true;
				}
				// Start/end times
				else {
					$dtstart_time = wp_date('His', $event->dtstart_array[2], $event_start_tz);
					// Conditional is for events that are missing DTEND altogether, which should never be the case but has been observed in customer support
					$dtend_time = wp_date('His', ($event->dtend_array[2] ?: $event->dtstart_array[2]), $event_end_tz);
					$all_day = false;
				}
				
				// Workaround for events in feeds that do not contain an end date/time
				if (empty($dtend_date)) { $dtend_date = isset($dtstart_date) ? $dtstart_date : null; }
				if (empty($dtend_time)) { $dtend_time = isset($dtstart_time) ? $dtstart_time : null; }
				
				// Mask info
				if (!empty($maskinfo)) {
					// Replace event title ("summary") with $maskinfo text string
					$event->summary = $maskinfo;
					// Remove other potentially sensitive information from the event
					$event->additionalProperties = array();
					$event->attach = '';
					$event->attendee = '';
					$event->categories = '';
					$event->color = '';
					$event->contact = '';
					$event->description = '';
					$event->geo = '';
					$event->location = '';
					$event->organizer = '';
					$event->resources = '';
					$event->url = '';
					$event->x_alt_desc = '';
				}
				
				// General event item details (regardless of all-day/start/end times)
				// Event description and other details have $maskinfo check in r34ics_has_desc() function
				$event_item = array(
					'attach' => $this->parse_attach_array($event->attach_array, $sametab),
					'categories' => ($event->categories ?: ''),
					'class' => ($event->class ?: ''),
					'color' => ($event->color ?: ''),
					'contact' => ($event->contact ?: ''),
					'dtend_date' => $dtend_date,
					'dtend_time' => $dtend_time,
					'dtend' => $event->dtend,
					'dtstamp' => $event->dtstamp,
					'dtstart_date' => $dtstart_date,
					'dtstart_time' => $dtstart_time,
					'dtstart' => $event->dtstart,
					'duration' => ($event->duration ?: ''),
					'eventdesc' => ($event->x_alt_desc ?: ($event->description ?: '')),
					'exdate' => ($event->exdate ?: ''),
					'feed_key' => $feed_key,
					'freebusy' => ($event->freebusy ?: ''),
					'geo' => ($event->geo ?: ''),
					'label' => ($event->summary ?: ''),
					'location' => ($event->location ?: ''),
					'organizer' => ($event->organizer_array ?: ''),
					'recurrence_id' => ($event->recurrence_id ?: ''),
					'resources' => ($event->resources ?: ''),
					'rrule' => ($event->rrule ?: ''),
					'rdate' => ($event->rdate ?: ''),
					'sequence' => ($event->sequence ?: ''),
					'status' => ($event->status ?: ''),
					'transp' => ($event->transp ?: ''),
					'uid' => ($event->uid ?: ''),
					'url' => ($event->url ?: ($event->uri ?: '')),
					'tz_start' => (!empty($args['eventlocaltime']) ? (@$event->dtstart_array[0]['TZID'] ?: $url_tz) : $url_tz),
					'tz_end' => (!empty($args['eventlocaltime']) ? (@$event->dtend_array[0]['TZID'] ?: $url_tz) : $url_tz),
				);
				
				// Scrape a URL from the description
				if (empty($event_item['url']) && !empty($event->description)) {
					$event_item['url'] = r34ics_scrape_url_from_string($event->description, false, true);
				}
				
				// Multiday events
				if	(
						$dtend_date != $dtstart_date &&
						// Watch out for events that are NOT multiday, but just extend into the next day, unless extendmultiday is set
						(
							in_array($args['extendmultiday'], array('both', 'overnight')) ||
							!($dtend_date == r34ics_date('Ymd', $dtstart_date, $event_start_tz, '+1 day') && !empty($dtend_time))
						)
					)
				{
					$loop_date = $dtstart_date;
					while ($loop_date <= $dtend_date) {
						// Classified as an all-day event and we've hit the end date -- don't display unless extendmultiday is set
						if ($all_day && $loop_date == $dtend_date && !in_array($args['extendmultiday'], array('both', 'allday'))) {
							break;
						}
						// Multiday events may be given with end date/time as midnight of the NEXT day (unless extendmultiday is set)
						$effective_end_date =	(!empty($all_day) && empty($dtend_time) && !in_array($args['extendmultiday'], array('both', 'allday')))
												? r34ics_date('Ymd', $dtend_date, $event_end_tz, '-1 day')
												: $dtend_date;
						if ($dtstart_date == $effective_end_date) {
							$ics_data['events'][$dtstart_date]['all-day'][] = $event_item;
							break;
						}
						// Get full date/time range of multiday event (to be used in displaying multiday events as single items in list views)
						$event_item['multiday'] = array(
							'start_date' => $dtstart_date,
							'start_time' => $dtstart_time,
							'end_date' => $effective_end_date,
							'end_time' => $dtend_time,
							'all_day' => $all_day,
						);
						if ($loop_date == $dtstart_date) {
							$event_item['multiday']['position'] = 'first';
						}
						elseif ($loop_date == $effective_end_date) {
							$event_item['multiday']['position'] = 'last';
						}
						else {
							$event_item['multiday']['position'] = 'middle';
						}
						$event_item['start'] = !empty($dtstart_time) ? r34ics_time_format($dtstart_time, $timeformat, $event_item['tz_start'], $dtstart_date) : null;
						$event_item['end'] = !empty($dtend_time) ? r34ics_time_format($dtend_time, $timeformat, $event_item['tz_end'], $dtend_date) : null;
						$ics_data['events'][$loop_date]['all-day'][] = apply_filters('r34ics_display_calendar_event_item', $event_item, $event, $args);
						// Increment loop date
						$loop_date = r34ics_date('Ymd', $loop_date, $event_start_tz, '+1 day');
					}
				}
				// All-day events
				elseif ($all_day) {
					$ics_data['events'][$dtstart_date]['all-day'][] = apply_filters('r34ics_display_calendar_event_item', $event_item, $event, $args);
				}
				// Events with start/end times
				else {
					$event_item['start'] = r34ics_time_format($dtstart_time, $timeformat, $event_item['tz_start'], $dtstart_date);
					$event_item['end'] = r34ics_time_format($dtend_time, $timeformat, $event_item['tz_end'], $dtend_date);
					$ics_data['events'][$dtstart_date]['t'.$dtstart_time][] = apply_filters('r34ics_display_calendar_event_item', $event_item, $event, $args);
				}
				
				// Append all recurrence instances
				if (count($events) > 1) {
					$event_item['recurrences'] = array();
					// Call it $item since we're already using $event
					foreach ((array)$events as $item) {
						$item_start_tz = (!empty($item->dtstart_array[0]['TZID']) ? @timezone_open($item->dtstart_array[0]['TZID']) : $url_tz);
						$date_format = get_option('date_format');
						if (strpos($date_format, 'l') === false) {
							$date_format = (strpos($date_format, ',') !== false ? 'l, ' : 'l ') . $date_format;
						}
						$event_item['recurrences'][] = array(
							'start' => wp_date('c', $item->dtstart_array[2], $item_start_tz),
							'end' => wp_date('c', $item->dtend_array[2], $item_end_tz),
						);
					}
				}

				return $event_item;
			}
			return false;
		}
		

		// Initiate REST API endpoint
		public function rest_api_init($wp_rest_server) {
			register_rest_route('r34icspro/v5', '/ics/' . $this->feed_uri_suffix, array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array(&$this, 'rest_api_callback_ics'),
					'permission_callback' => '__return_true',
			));
			register_rest_route('r34icspro/v5', '/ics/' . $this->feed_uri_suffix . '/(?P<category>[a-zA-Z0-9\-\_]+)', array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array(&$this, 'rest_api_callback_ics'),
					'permission_callback' => '__return_true',
			));
			register_rest_route('r34icspro/v5', '/ics_event/' . $this->feed_uri_suffix . '/(?P<id>\d+)', array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array(&$this, 'rest_api_callback_ics'),
					'permission_callback' => '__return_true',
			));
			register_rest_route('r34icspro/v5', '/ics_admin/' . $this->feed_uri_suffix . '/(?P<nonce>[a-z0-9\-]+)', array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array(&$this, 'rest_api_callback_ics_admin'),
					'permission_callback' => array(&$this, 'rest_api_permission_callback_ics_admin'),
			));
			register_rest_route('r34icspro/v5', '/ics_admin/' . $this->feed_uri_suffix . '/(?P<nonce>[a-z0-9\-]+)/(?P<category>[a-zA-Z0-9\-\_]+)', array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array(&$this, 'rest_api_callback_ics_admin'),
					'permission_callback' => array(&$this, 'rest_api_permission_callback_ics_admin'),
			));
		}
		
		
		// REST API callback for public requests
		public function rest_api_callback_ics($request) {
			return rest_ensure_response($this->get_feed($request['category'] ?? null, $request['id'] ?? null, false));
		}
		
		
		// REST API callback for admin
		public function rest_api_callback_ics_admin($request) {
			return rest_ensure_response($this->get_feed($request['category'] ?? null, $request['id'] ?? null, true));
		}
		
		
		// REST API permission callback for admin
		public function rest_api_permission_callback_ics_admin($request) {
			/**
			 * Note: This is all happening server-side, so we can't rely on the regular nonce
			 * functions. Instead we write this nonce to a transient so we can check it later
			 * ourselves as *part* of the verification process. We also verify that the request
			 * is coming from the server itself. (NOT the client IP address!)
			 */
			$valid = false;
			$nonce = $request->get_param('nonce');
			if (!empty($nonce) && get_transient('r34icspro_admin_nonce_' . $nonce)) {
				if (get_option('r34icspro_event_admin_nonce_bypass_ip_check')) {
					$valid = true;
				}
				else {
					$server_addr = !empty($_SERVER['SERVER_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) : '';
					$remote_addr = !empty($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
					$allowed_ips = array(
						'127.0.0.1',
						'::1', // @todo Verify that IPv6 is applicable here
						$server_addr,
					);
					$valid = in_array($remote_addr, $allowed_ips);
					// Look up the domain if first check failed (avoids unnecessary lookups)
					if (empty($valid)) {
						$lookup = gethostbyname(wp_parse_url(home_url('/'), PHP_URL_HOST));
						$valid = ($remote_addr == $lookup);
					}
				}
			}
			return $valid;
		}
		
		
		// ICS Event CPT: Get event start or end datetime in iCalendar format
		private function _get_ics_datetime($post_id, $start_or_end='start') {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			ini_set('display_errors', 'off');
			$fields = $this->get_custom_fields($post_id);
			if (!in_array($start_or_end, array('start', 'end'))) { return false; }
			$tzid = $fields['tzid'] ?? get_option('timezone_string');
			$output = '';
			if (!empty($fields['all_day']) && !empty($fields[$start_or_end . '_date'])) {
				// Append one day for end date since iCalendar expects date *after* end
				$output = 'VALUE=DATE:' . r34ics_date('Ymd', $fields[$start_or_end . '_date'], $tzid, ($start_or_end == 'end' ? '+1 day' : ''));
			}
			elseif (!empty($fields[$start_or_end . '_datetime'])) {
				$output = 'TZID=' . $tzid . ':' . r34ics_date('Ymd\THis', $fields[$start_or_end . '_datetime'], $tzid);
			}
			return $output;
		}
		
		
		// ICS Event CPT: Get event exception date for recurrence rule in iCalendar format
		private function _get_ics_exdate($post_id) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			ini_set('display_errors', 'off');
			$fields = $this->get_custom_fields($post_id);
			if (empty($fields['rrule'])) { return false; }
			if (!empty($fields['rrule_exdates'])) {
				$output = '';
				// Get recursive ACF data
				// @todo Encapsulate this in a function
				$exdates_count = intval($fields['rrule_exdates']);
				$exdates = array();
				for ($i = 0; $i < $exdates_count; $i++) {
					$exdates[] = get_post_meta($post_id, 'rrule_exdates_' . $i . '_rrule_exdate', true);
				}
				$tzid = $fields['tzid'] ?? get_option('timezone_string');
				// Does this event have a start time?
				// Note: We check both because ACF retains the data even if conditionals have been switched
				if (empty($fields['all_day']) && !empty($fields[$start_or_end . 'start_datetime'])) {
					$output .= 'TZID=' . $tzid . ';VALUE=DATE-TIME:';
					$time = r34ics_date('His', $fields['start_datetime'], $tzid);
					foreach ((array)$exdates as $key => $value) {
						$exdates[$key] = $value . 'T' . $time;
					}
				}
				else {
					$output .= 'TZID=' . $tzid . ';VALUE=DATE:';
				}
				$output .= implode(',', (array)$exdates);
				return $output;
			}
			return '';
		}
		
		
		// ICS Event CPT: Get a general field in iCalendar format
		private function _get_ics_field($post_id, $field, $force_uppercase=false, $filter_content=false) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			ini_set('display_errors', 'off');
			$fields = $this->get_custom_fields($post_id);
			$output = '';
			if (!empty($fields[$field])) {
				$output = !empty($force_uppercase) ? strtoupper($fields[$field]) : $fields[$field];
			}
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
			return trim(preg_replace('/[\s]+/', ' ', ($filter_content ? apply_filters('the_content', $output) : $output)));
		}
		
		
		// ICS Event CPT: Get event recurrence rule in iCalendar format
		private function _get_ics_rrule($post_id) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			ini_set('display_errors', 'off');
			$fields = $this->get_custom_fields($post_id);
			if (empty($fields['rrule'])) { return false; }
			$output = 'FREQ=' .$fields['rrule_freq'] . ';INTERVAL=' . $fields['rrule_interval'] . ';';
			switch ($fields['rrule_freq']) {
				case 'DAILY':
					break;
				case 'WEEKLY':
					$output .= 'WKST=' . r34icspro_dow_2_letter_4_ics(get_option('start_of_week')) . ';BYDAY=' . implode(',', unserialize($fields['rrule_week_days']) ?: (array)$fields['rrule_week_days']) . ';';
					break;
				case 'MONTHLY':
					switch ($fields['rrule_month_by']) {
						case 'exact date':
							// @todo Determine why this unserialize() generates "Error at offset 0 of 2 bytes" even though it is working
							$output .= 'BYMONTHDAY=' . implode(',', unserialize($fields['rrule_month_exact_date']) ?: (array)$fields['rrule_month_exact_date']) . ';';
							break;
						case 'week and day':
							// Unserialize and concatenate week and day rules
							$byday = array();
							$weeks_arr = unserialize($fields['rrule_month_week']) ?: (array)$fields['rrule_month_week'];
							$days_arr = unserialize($fields['rrule_month_week_days']) ?: (array)$fields['rrule_month_week_days'];
							foreach ((array)$weeks_arr as $week) {
								foreach ((array)$days_arr as $day) {
									$byday[] = '' . intval($week) . preg_replace('[^A-Z]', '', $day);
								}
							}
							$output .= 'BYDAY=' . implode(',', $byday) . ';';
							break;
						default: break;
					}
					break;
			}
			switch ($fields['rrule_end']) {
				case 'on date':
					$output .= 'UNTIL=' . r34ics_date('Ymd\THis\Z', $fields['rrule_end_date']) . ';';
					break;
				case 'after':
				case 'after...': // `rrule_end` value shouldn't have had ellipses, but it does
					$output .= 'COUNT=' . $fields['rrule_count'] . ';';
					break;
				case 'never': default: break;
			}
			return rtrim($output, ';');
		}


		// ICS Event CPT: Get event location in iCalendar format
		private function _get_ics_taxonomy($post_id, $taxonomy='', $name_only=false) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			$taxonomies = array('r34icspro_event_category', 'r34icspro_event_location', 'r34icspro_event_organizer');
			if (!in_array($taxonomy, $taxonomies)) { return ''; }
			if ($terms = get_the_terms($post_id, $taxonomy)) {
				$output = array();
				foreach ((array)$terms as $term) {
					if (empty($name_only) && !empty(trim($term->description))) {
						$output[] = trim($term->description);
					}
					else {
						$output[] = trim($term->name);
					}
				}
				return trim(preg_replace('/[\s]+/', ' ', wp_strip_all_tags(implode(', ', $output))));
			}
			return '';
		}
		
				
		// ICS Event CPT: Get event UID
		// Based on: https://www.uuidgenerator.net/dev-corner/php
		private function _get_ics_uid($post_id) {
			$uid = 'r34ics-' . sha1($post_id . __METHOD__ . get_bloginfo('url')) . '-' . intval($post_id);
			// Convert string to UUID format to resolve issues with Google Calendar imports
			return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($uid), 4));
		}
		
		
		// ICS Event CPT: Generate a URL suffix to obfuscate REST API URLs
		private function _get_ics_uri_suffix() {
			$suffix = get_option('r34icspro_ics_events_url_suffix');
			if (empty($suffix) || !ctype_xdigit($suffix)) {
				// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
				$suffix = sha1(uniqid(wp_unslash($_SERVER['SERVER_NAME'] ?? ''), true));
				// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
				update_option('r34icspro_ics_events_url_suffix', $suffix, false);
			}
			return $suffix;
		}
		
	}

}
