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

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

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

	class R34ICSPro extends R34ICS {

		public $version = ''; // Value loads automatically as of v.4.5.0
		
		public $admin_sidebar_path = null;
		
		public $illustration_base_url = 'https://ssl.gstatic.com/calendar/images/eventillustrations/v1/';
		
		public $icon_svg = '<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m151.805 23.273h-72.832v-9.309c0-7.707-6.257-13.964-13.964-13.964s-13.963 6.257-13.963 13.964v9.743c-28.749 3.521-51.046 28.051-51.046 57.748v116.363c0 32.112 26.071 58.182 58.182 58.182h116.364c32.111 0 58.182-26.07 58.182-58.182v-116.363c0-30.365-23.311-55.328-52.996-57.954v-9.537c0-7.707-6.257-13.964-13.963-13.964-7.707 0-13.964 6.257-13.964 13.964zm-129.507 172.613c0 20.333 16.508 36.842 36.841 36.842h112.499c20.334 0 36.842-16.509 36.842-36.842v-102.793h-186.182zm141.388 16.246c15.069 0 29.188-7.467 29.188-23.215 0-25.658-37.062-19.278-37.062-27.966 0-2.444 2.851-4.752 7.738-4.752 4.48 0 7.874 2.037 7.874 5.431v1.357h20.092v-1.357c0-13.169-11.267-22.808-27.83-22.808-16.427 0-27.966 9.503-27.966 23.487 0 25.115 36.926 18.191 36.926 27.83 0 2.851-3.394 4.616-8.145 4.616-5.159 0-9.503-2.172-9.503-6.109v-1.63h-20.228v1.63c0 13.575 11.539 23.486 28.916 23.486zm-63.942 0c18.191 0 30.138-11.404 30.138-28.781v-2.036h-20.5v2.579c0 6.245-3.936 9.91-9.638 9.91-7.195 0-10.725-4.072-10.725-11.811v-13.032c0-7.739 3.665-11.811 10.725-11.811 5.702 0 9.638 3.665 9.638 9.91v2.579h20.5v-2.036c0-17.377-11.947-28.781-30.274-28.781-19.278 0-30.953 11.676-30.953 30.003v13.304c0 18.328 11.675 30.003 31.089 30.003zm-60.413-2.037h20.364v-69.236h-20.364zm0-76.567h20.364v-19.821h-20.364z" transform="matrix(.0944206 0 0 .0933852 1 0)" fill="currentColor"/></svg>';
				
		public $palettes = array(
			// True neutral grays (default)
			'neutral' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#f5f5f5', // whitesmoke rgb(245,245,245)
				'gainsboro' => '#dcdcdc', // gainsboro rgb(220,220,220)
				'darkgray' => '#a9a9a9', // darkgray rgb(169,169,169)
				'gray' => '#808080', // gray rgb(128,128,128)
				'dimgray' => '#696969', // dimgray rgb(105,105,105)
				'black' => '#000000', // black rgb(0,0,0)
			),
			// Cool grays
			'cool' => array(
				'white' => '#ffffff', // rgb(255,255,255)
				'whitesmoke' => '#f0f3f6', // rgb(240,243,246)
				'gainsboro' => '#d9dcdf', // rgb(217,220,223)
				'darkgray' => '#a3a8ac', // rgb(163,168,172)
				'gray' => '#70787f', // rgb(112,120,127)
				'dimgray' => '#60686f', // rgb(96,104,111)
				'black' => '#10181f', // rgb(16,24,31)
			),
			// Warm grays
			'warm' => array(
				'white' => '#ffffff', // rgb(255,255,255)
				'whitesmoke' => '#f6f3f0', // rgb(246,243,240)
				'gainsboro' => '#dfdcd9', // rgb(223,220,217)
				'darkgray' => '#aca8a3', // rgb(172,168,163)
				'gray' => '#7f7870', // rgb(127,120,112)
				'dimgray' => '#6f6860', // rgb(111,104,96)
				'black' => '#1f1810', // rgb(31,24,16)
			),
			// ICS Calendar Pro brand colors
			'ICS Calendar Pro' => array(
				'white' => '#ffffff',
				'whitesmoke' => '#f0f3f6',
				'gainsboro' => '#d9dcdf',
				'darkgray' => '#0099ff',
				'gray' => '#ee0055',
				'dimgray' => '#3300bb',
				'black' => '#10181f',
			),
			// Boardroom
			'boardroom' => array(
				'white' => '#ffffff',
				'whitesmoke' => '#e6e5e0',
				'gainsboro' => '#d57e25',
				'darkgray' => '#bdbcb7',
				'gray' => '#4d5c6f',
				'dimgray' => '#7b7b79',
				'black' => '#181818',
			),
			// Wheat Field
			'wheat field' => array(
				'white' => '#ffffff',
				'whitesmoke' => '#f6e9bf',
				'gainsboro' => '#ffa561',
				'darkgray' => '#7f7d20',
				'gray' => '#406cad',
				'dimgray' => '#413e13',
				'black' => '#181818',
			),
			// Rose Garden
			'rose garden' => array(
				'white' => '#ffffff',
				'whitesmoke' => '#fcdef8',
				'gainsboro' => '#f3438b',
				'darkgray' => '#d61f5f',
				'gray' => '#305b3d',
				'dimgray' => '#664218',
				'black' => '#181818',
			),
			// Larch Forest
			'larch forest' => array(
				'white' => '#ffffff',
				'whitesmoke' => '#fbf3c9',
				'gainsboro' => '#be9d4c',
				'darkgray' => '#721d30',
				'gray' => '#567543',
				'dimgray' => '#524d4b',
				'black' => '#181818',
			),
			// Yellows and oranges
			'sunrise' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#fff7cd', // 
				'gainsboro' => '#ffd700', // gold rgb(255,215,0)
				'darkgray' => '#ff8e00', // 
				'gray' => '#ff4500', // orangered rgb(255,69,0)
				'dimgray' => '#7f2200', // 
				'black' => '#181818', // 
			),
			// Blues
			'midday' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#e7f5fe', // 
				'gainsboro' => '#87cefa', // lightskyblue rgb(135,206,250)
				'darkgray' => '#66a8d7', // 
				'gray' => '#4682b4', // steelblue rgb(70,130,180)
				'dimgray' => '#23415a', // 
				'black' => '#181818', // 
			),
			// Reds
			'sunset' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#fef1f3', // 
				'gainsboro' => '#ffb6c1', // lightpink rgb(255,182,193)
				'darkgray' => '#ee657e', // 
				'gray' => '#dc143c', // crimson rgb(220,20,60)
				'dimgray' => '#6f091e', // 
				'black' => '#181818', // 
			),
			// Midnight
			'midnight' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#e6dff5', // 
				'gainsboro' => '#9370db', // mediumpurple rgb(147,112,219)
				'darkgray' => '#624db0', // 
				'gray' => '#312a85', // 
				'dimgray' => '#191970', // midnightblue rgb(25,25,112)
				'black' => '#000000', // black rgb(0,0,0)
			),
			// Greens
			'spring' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#eafeeb', // 
				'gainsboro' => '#98fb98', // palegreen rgb(152,251,152)
				'darkgray' => '#4caf4c', // 
				'gray' => '#006400', // darkgreen rgb(0,100,0)
				'dimgray' => '#003200', // 
				'black' => '#181818', // 
			),
			// Pinks and purples
			'summer' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#f8ecf8', // 
				'gainsboro' => '#dda0dd', // plum rgb(221,160,221)
				'darkgray' => '#cb7ad8', // 
				'gray' => '#ba55d3', // mediumorchid rgb(186,85,211)
				'dimgray' => '#7c2c7c', // 
				'black' => '#181818', // 
			),
			// Browns
			'autumn' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#fdf8f0', // 
				'gainsboro' => '#f5deb3', // wheat rgb(245,222,179)
				'darkgray' => '#e2a368', // 
				'gray' => '#d2691e', // chocolate rgb(210,105,30)
				'dimgray' => '#69340f', // 
				'black' => '#181818', // 
			),
			// Blue-grays
			'winter' => array(
				'white' => '#ffffff', // white rgb(255,255,255)
				'whitesmoke' => '#eff3f9', // 
				'gainsboro' => '#b0c4de', // lightsteelblue rgb(176,196,222)
				'darkgray' => '#94a6bb', // 
				'gray' => '#778899', // lightslategray rgb(119,136,153)
				'dimgray' => '#3b444c', // 
				'black' => '#181818', // 
			),
			// Custom (defaults to ICS Calendar Pro palette but loads saved custom colors on init)
			'custom' => array(
				'white' => '#ffffff',
				'whitesmoke' => '#f0f3f6',
				'gainsboro' => '#d9dcdf',
				'darkgray' => '#0099ff',
				'gray' => '#ee0055',
				'dimgray' => '#3300bb',
				'black' => '#10181f',
			),
		);
		
		public $palette_labels_i18n = array();
		
		protected $_convert_grid_to_full = true;
		protected $_dev_mode = false;
		protected $_beta_updates = false;

		protected $_pro_shortcode_defaults = array(
			'admin_nonce' => '',
			'availability_only' => false,
			'basic_style' => '',
			'booked_day_bg_color' => '',
			'calendar_illustrations' => false,
			'combined_availability' => false,
			'empty_day_bg_color' => 'transparent',
			'empty_day_description' => '',
			'empty_day_title' => '',
			'eventdesc_length' => '',
			'fc_business_days' => null,
			'fc_height' => '',
			'fc_hide_alldayslot' => false,
			'fc_no_overlap' => false,
			'fc_tab_default' => '',
			'fc_tab_default_mobile' => '',
			'fc_tab_mobile_default_list' => false,
			'fc_tabs' => null,
			'fc_tabs_mobile' => null,
			'fc_timegrid_all_hours' => false,
			'fc_year_tab_max_columns' => 3,
			'fill_empty_days' => false,
			'filters' => false,
			'hiddenonload' => false,
			'hideweekends' => false,
			'hours' => '',
			'id' => '',
			'limit_in_table' => 3,
			'masonry_grouping' => 'event',
			'month_list_all' => false,
			'month_table_list_toggle' => false,
			'no_ajax' => false,
			'no_angled_backgrounds' => false,
			'no_sidebar_scroll' => false,
			'pagination_auto_cycle' => false,
			'regex_pattern' => '',
			'regex_replacement' => '',
			'regex_scope' => '',
			'regex' => false,
			'showfilter' => false,
			'subscribelink' => false,
			'table_cell_contents' => '',
			'tzinlegend' => false,
			'upnext_future_label' => '',
			'upnext_now_label' => '',
			'upnext_show_future' => false,
			'upnext_show_date' => false,
			'use_client_tz' => false,
			'zoom' => 0.6,
		);
		
		protected $_pro_views = array(
			'full',
			'grid', // Removed, but still needed here for $this->_convert_grid_to_full() to work
			'masonry',
			'month-with-sidebar',
			'up-next',
			'widget',
			'widget-availability',
			'year-availability',
			'year-with-sidebar',
		);

		
		public function __construct() {
		
			// Set version
			$this->version = $this->_get_version();

			// 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';

			// Define allowed views (must load later so filters can be applied by other plugins)
			add_action('init', function() {
				$this->views = apply_filters('r34ics_views', $this->views);
				$this->list_style_views = apply_filters('r34ics_list_style_views', $this->list_style_views);
			}, 11);
		
			// Set dev mode
			$this->_dev_mode = defined('R34ICSPRO_DEV_MODE');
				
			// Set beta updates
			$this->_beta_updates = get_option('r34icspro_beta_updates', defined('R34ICSPRO_BETA_UPDATES'));
				
			// Set up admin bar
			//add_action('admin_bar_menu', array(&$this, 'admin_bar_pro_menu'), 99, 1); // Reserved for future use

			// Enqueue admin scripts
			add_action('admin_enqueue_scripts', array(&$this, 'admin_enqueue_pro_scripts'));
			
			// Resolve script conflicts
			add_action('admin_enqueue_scripts', array(&$this, 'admin_enqueue_pro_scripts_resolve_conflicts'), PHP_INT_MAX - 1, 0);
			
			// Check for use of deprecated features that will be removed soon
			add_action('admin_init', array(&$this, 'deprecated_feature_check'));
	
			// Update locale
			add_action('admin_init', array(&$this, 'maybe_update_locale'));
		
			// Set up admin menu
			add_action('admin_menu', array(&$this, 'admin_pro_menu'));
			
			// Add admin notices
			add_action('admin_notices', array(&$this, 'admin_pro_notices'), PHP_INT_MAX - 1);
			
			// Add ICS Calendar Pro admin header
			add_action('in_admin_header', array(&$this, 'admin_header'));
			
			// Load ACF PRO
			/**
			 * Note: We are loading this absolutely as early as possible to avoid WP 6.7
			 * warnings. Embedded ACF PRO must load AFTER translations; it is now loading on
			 * 'init' with priority 2 - PHP_INT_MAX which appears to be early enough for it to
			 * function properly; translations are loading on 'init' with priority
			 * 1 - PHP_INT_MAX.
			 *
			 * Changing the loading sequence has been identified to cause conflicts with at
			 * least one third-party theme (Memberoni). The r34icspro_early_acf_init option was
			 * provided as a way to override the recommended loading sequence and revert to the
			 * previous sequence. This option was removed in version 6.0
			 */
			add_action('init', array(&$this, 'acf_init'), 2 - PHP_INT_MAX);
			
			// Color palette
			add_action('init', array(&$this, 'color_palette_init'));

			// Register custom post type
			add_action('init', array(&$this, 'custom_post_type'));
			
			// Register blocks
			add_action('init', array(&$this, 'register_blocks'));
			
			// Add plugin action links
			add_filter('plugin_action_links_ics-calendar-pro/ics-calendar-pro.php', array(&$this, 'admin_pro_plugin_action_links'));
			add_filter('plugin_row_meta', array(&$this, 'admin_pro_plugin_row_meta'), 10, 4);

			// Add query variables
			add_filter('query_vars', function($query_vars) {
				$query_vars[] = 'icstz'; // Allows passing a timezone value in the query string
				return $query_vars;
			});
			
			// R34ICS actions
			add_action('r34ics_admin_add_calendar_settings_html', array(&$this, 'admin_add_calendar_settings_html'), 10, 0);
			add_action('r34ics_admin_getting_started_links_after', array(&$this, 'admin_getting_started_links_after'), 10, 0);
			add_action('r34ics_admin_getting_started_links_before', array(&$this, 'admin_getting_started_links_before'), 10, 0);
			add_action('r34ics_color_key_html_after_feed_title', array(&$this, 'color_key_html_after_feed_title'), 10, 3);
			add_action('r34ics_display_calendar_after_render_template', array(&$this, 'display_calendar_after_render_template'), 10, 3);
			add_action('r34ics_display_calendar_after_wrapper', array(&$this, 'display_calendar_after_wrapper'), 10, 3);
			add_action('r34ics_display_calendar_after_args', array(&$this, 'apply_calendar_builder_filters'), 10, 1);
			add_action('r34ics_display_calendar_before_render_template', array(&$this, 'display_calendar_before_render_template'), 10, 3);
			add_action('r34ics_display_calendar_before_wrapper', array(&$this, 'display_calendar_before_wrapper'), 10, 3);
			add_action('r34ics_display_calendar_render_template', array(&$this, 'display_calendar_render_template'), 10, 3);
			add_action('r34ics_enqueue_external_scripts', array(&$this, 'enqueue_pro_scripts'));		
			//add_action('r34ics_event_description_html', array(&$this, 'event_description_html_pro'), 10, 4); // Avoid method name collision // Reserved for future use
			add_action('r34ics_event_label_html', '__return_false', 10, 3);
			add_action('r34ics_event_sublabel_html', '__return_false', 10, 3);
			add_action('r34ics_purge_calendar_transients', array(&$this, 'purge_pro_calendar_transients'));
			add_action('r34ics_register_external_scripts', array(&$this, 'register_pro_scripts'));		
			add_action('r34ics_reset_non_empty_defaults', array(&$this, 'reset_non_empty_defaults'));
			add_action('r34ics_shortcode_before_display_calendar', '__return_false', 10, 1);
			add_action('r34ics_shortcode_after_display_calendar', '__return_false', 10, 2);
			
			// R34ICS filters
			add_filter('r34ics_ajax_js_args', array(&$this, 'ajax_js_args'), 10, 2);
			add_filter('r34ics_calendar_classes', array(&$this, 'calendar_classes'), 10, 3);
			add_filter('r34ics_calendar_view', array(&$this, 'calendar_view'), 10, 2);
			add_filter('r34ics_debug_array', array(&$this, 'debug_array'), 10, 1);
			add_filter('r34ics_display_add_calendar_button', array(&$this, 'display_add_calendar_button'), 10, 1);
			add_filter('r34ics_display_calendar_args', array(&$this, 'display_calendar_args'), 10, 2);
			add_filter('r34ics_display_calendar_color_set', array(&$this, 'display_calendar_color_set'), 10, 2);
			add_filter('r34ics_display_calendar_early_render', array(&$this, 'display_calendar_early_render'), 10, 2);
			//add_filter('r34ics_display_calendar_event_item', array(&$this, 'display_calendar_event_item', 10, 3); // Reserved for future use
			add_filter('r34ics_display_calendar_exclude_event', '__return_false', 1, 3); // Priority 1 allows users' customizations to override
			add_filter('r34ics_display_calendar_filter_ics_data', array(&$this, 'display_calendar_filter_ics_data'), 10, 2);
			add_filter('r34ics_display_calendar_ics_data_init', array(&$this, 'display_calendar_ics_data_init_pro'), 10, 2);
			add_filter('r34ics_display_calendar_preprocess_raw_feed', array(&$this, 'display_calendar_preprocess_raw_feed'), 10, 4);
			add_filter('r34ics_display_calendar_range_end', array(&$this, 'display_calendar_range_end'), 10, 2);
			add_filter('r34ics_display_calendar_range_start', array(&$this, 'display_calendar_range_start'), 10, 2);
			add_filter('r34ics_display_calendar_set_earliest', array(&$this, 'display_calendar_set_earliest'), 10, 2);
			add_filter('r34ics_display_calendar_set_first_date', array(&$this, 'display_calendar_set_first_date'), 10, 4);
			add_filter('r34ics_display_calendar_set_latest', array(&$this, 'display_calendar_set_latest'), 10, 5);
			add_filter('r34ics_display_calendar_set_limit_date', array(&$this, 'display_calendar_set_limit_date'), 10, 4);
			add_filter('r34ics_event_description_html_filter', array(&$this, 'event_description_html_filter'), 10, 5);
			add_filter('r34ics_event_label_html_filter', array(&$this, 'event_label_html_filter'), 10, 4);
			add_filter('r34ics_event_sublabel_html_filter', array(&$this, 'event_sublabel_html_filter'), 10, 4);
			add_filter('r34ics_eventdesc_include_time_and_title', array(&$this, 'eventdesc_include_time_and_title'), 10, 3);
			add_filter('r34ics_feed_colors_css', array(&$this, 'feed_colors_css'), 10, 4);
			add_filter('r34ics_has_desc', array(&$this, 'has_desc'), 10, 3);
			add_filter('r34ics_list_style_views', array(&$this, 'list_style_views'), 10, 1);
			add_filter('r34ics_shortcode_defaults_merge', array(&$this, 'shortcode_defaults_merge_pro'), 10, 1); // Avoid method name collision
			add_filter('r34ics_shortcode_defaults_new_10_6', array(&$this, 'shortcode_defaults_new_10_6'), 10, 1);
			add_filter('r34ics_shortcode_feed_array_params', array(&$this, 'shortcode_feed_array_params'), 10, 1);
			add_filter('r34ics_system_report_array', array(&$this, 'system_report_array'), 10, 1);
			add_filter('r34ics_views', array(&$this, 'pro_views'), 10, 1);

			// R34ICSPro filters
			add_filter('r34icspro_calendar_illustration_keywords', array(&$this, 'illustrations_library_keywords'), 10, 1);

		}
		
		
		public function acf_init() {
		
			// Determine which version of ACF we're using
			$acf_status = r34icspro_acf_status();
			
			// Load our version of ACF PRO if the necessary conditions are met
			global $r34icspro_acf_bypass;
			$is_r34icspro = (r34icspro_is_r34icspro() || wp_doing_ajax()); // @todo Investigate if we still need wp_doing_ajax() here!
			if (
				// Site doesn't already have ACF PRO installed
				$acf_status != 'pro' &&
				(
					// We're using embedded ACF PRO, and this is an ICS Calendar Pro page
					($acf_status == 'embedded' && $is_r34icspro) ||
					// Site has ACF free installed, but this is an ICS Calendar Pro page
					// Note: mu-plugins/r34icspro-no-acf.php sets the bypass variable; needs to run early to deactivate ACF free
					(in_array(r34icspro_acf_status(), array('free', 'other')) && $is_r34icspro && !empty($r34icspro_acf_bypass))
				)
			)
			{
					include_once(plugin_dir_path(dirname(__FILE__)) . 'vendors/acf/acf.php');
					add_filter('acf/settings/path', function($path) { return plugin_dir_path(dirname(__FILE__)) . 'vendors/acf/'; });
					add_filter('acf/settings/url', function($url) { return plugin_dir_url(dirname(__FILE__)) . 'vendors/acf/'; });
					if ($acf_status == 'embedded') {
						add_filter('acf/settings/show_admin', '__return_false');
						add_filter('acf/settings/show_updates', '__return_false', 100);
						/**
						 * Note: acf/settings/show_admin set to false does not actually remove the CPT
						 * editing pages, just their admin menu links. This removes the pages entirely.
						 */
						add_filter('register_post_type_args', function($args, $cpt) {
							switch ($cpt) {
								case 'acf-field-group':
								case 'acf-post-type':
								case 'acf-taxonomy':
								case 'acf-ui-options-page':
									$args['show_ui'] = false;
									break;
								default:
									break; // Do nothing
							}
							return $args;
						}, 10, 2);
					}
			}
		
			// Perform additional actions after ACF is initialized
			add_action('acf/init', array(&$this, 'acf_init_callback'));

		}
		
		
		public function acf_init_callback() {
		
			// Load our ACF field groups, only when needed	
			add_action('current_screen', function() {
				if (function_exists('get_current_screen') && $current_screen = get_current_screen()) {
					// Calendar Builder
					if ($current_screen->id == 'r34icspro_calendar') {
						include_once(plugin_dir_path(dirname(__FILE__)) . 'field_groups/field_groups_calendar_builder.php');
					}
	
					// Illustrations Library
					if ($current_screen->base == 'r34icspro_calendar_page_r34icspro-illustrations-library') {
						include_once(plugin_dir_path(dirname(__FILE__)) . 'field_groups/field_groups_illustrations_library.php');
					}
				}
			});
			
		}
		
		
		public function admin_bar_pro_menu($wp_admin_bar) {
			/*
			if (function_exists('r34ics_admin_full_access') && r34ics_admin_full_access()) {
				$wp_admin_bar->add_menu(array(
					'href' => false,
					'id' => 'r34icspro-admin-bar',
					'title' => __('ICS Calendar', 'ics-calendar-pro'),
				));
				$wp_admin_bar->add_node(array(
					'href' => admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings&utility=clear_cache#utilities'),
					'id' => 'r34icspro-admin-bar-clear-cache',
					'parent' => 'r34icspro-admin-bar',
					'title' => __('Clear Cached Calendar Data', 'ics-calendar-pro'),
				));
			}
			*/
		}
		
		
		public function admin_add_calendar_settings_html() {
			include_once(plugin_dir_path(dirname(__FILE__)) . 'templates/admin/add_calendar_settings_html.php');
		}


		public function admin_enqueue_pro_scripts() {
			$current_screen = get_current_screen();

			// General ICS Calendar Pro admin scripts and styles
			wp_enqueue_script('ics-calendar-pro-admin', plugin_dir_url(dirname(__FILE__)) . 'assets/admin-script-pro.min.js', array('jquery'), get_option('r34icspro_version'), true);
			wp_enqueue_style('ics-calendar-pro-admin', plugin_dir_url(dirname(__FILE__)) . 'assets/admin-style-pro.min.css', false, get_option('r34icspro_version'));
			
			// Hide "Add New" buttons if unlicensed
			if (!$this->licensed()) {
				wp_add_inline_style('ics-calendar-pro-admin', 'a[href*="post-new.php?post_type=r34icspro_calendar"] { display: none !important; }');
			}

			// Scripts and styles for Settings & Help page only
			if (isset($current_screen->base) && $current_screen->base == 'r34icspro_calendar_page_ics-calendar-pro-settings') {
	
				// WP Color Picker for Appearance
				wp_enqueue_script('wp-color-picker');
				wp_enqueue_style('wp-color-picker');
				
				// Custom JavaScript for Appearance
				// @todo Minify
				wp_enqueue_script('ics-calendar-pro-admin-appearance', plugin_dir_url(dirname(__FILE__)) . 'assets/admin-script-pro-appearance.min.js', array('jquery', 'wp-color-picker'), get_option('r34icspro_version'), true);

			}

			// Add inline scripts (dynamic values)
			wp_add_inline_script('ics-calendar-pro-admin', 'var r34icspro_admin_vars = ' . json_encode(array(
				'ics_events_step_minute' => get_option('r34icspro_ics_events_step_minute', 5),
				'i18n' => array(
					'Copy to clipboard...' => __('Copy to clipboard...', 'ics-calendar-pro'),
					'Copied' => __('Copied', 'ics-calendar-pro'),
					'End date/time must occur after start date/time. Please select another end date/time.' => __('End date/time must occur after start date/time. Please select another end date/time.', 'ics-calendar-pro'),
					'Please select a saved calendar from the menu.' => __('Please select a saved calendar from the menu.', 'ics-calendar-pro'),
				),
				'licensed' => intval($this->licensed()),
				'locale' => str_replace('_', '-', get_locale()),
				'palettes' => (array)$this->palettes,
				'plugin_url' => esc_url(WP_PLUGIN_URL) . '/ics-calendar-pro/',
				'version' => esc_attr($this->version),
			)) . ';');

		}


		public function admin_enqueue_pro_scripts_resolve_conflicts() {
			$current_screen = get_current_screen();

			// Calendar Builder CPT
			// @todo Reserved for possible future use
			//if (@$current_screen->base == 'post' && @$current_screen->post_type == 'r34icspro_calendar') {
			//}

			// ICS Events CPT
			if (@$current_screen->base == 'post' && @$current_screen->post_type == 'r34icspro_event') {
				// WP Events Manager -- dequeue conflicting date/time picker
				wp_dequeue_script('wpems-admin-datetimepicker-full');
				wp_dequeue_style('wpems-admin-datetimepicker-min');
			}

		}
				
		
		public function admin_getting_started_links_after() {
			?>
			<li>
				<h3 style="margin-bottom: 0.25rem;">ICS Calendar Pro:<?php /* Do not translate */ ?></h3>
				<ul>
				<li><a href="https://icscalendar.com/calendar-builder/" target="_blank"><?php esc_html_e('Calendar Builder', 'ics-calendar-pro'); ?></a></li>
				<li><a href="https://icscalendar.com/ics-events-documentation/" target="_blank"><?php esc_html_e('ICS Events', 'ics-calendar-pro'); ?></a></li>
				<li><a href="https://icscalendar.com/support/" target="_blank"><?php esc_html_e('Support Request Form', 'ics-calendar-pro'); ?></a></li>
				</ul>
			</li>
			<?php
		}
		
		
		public function admin_getting_started_links_before() {
		}
		
		
		public function admin_header() {
			$current_screen = function_exists('get_current_screen') ? get_current_screen() : null;
			if (is_object($current_screen) && strpos($current_screen->id, 'r34ics') !== false) {
				include_once(plugin_dir_path(dirname(__FILE__)) . 'templates/admin/admin_header.php');
			}
		}
		
		
		public function admin_pro_menu() {
			$licensed = $this->licensed();
			
			// Hide "Add New" link if unlicensed
			if (empty($licensed)) {
				remove_submenu_page('edit.php?post_type=r34icspro_calendar','post-new.php?post_type=r34icspro_calendar');
			}
			
			// Only show additional options if licensed
			if (!empty($licensed)) {
				if (function_exists('acf_add_options_page')) {
					acf_add_options_page(array(
						'page_title' => 'ICS Calendar Pro ' . __('Illustrations Library', 'ics-calendar-pro'),
						'menu_title' => __('Illustrations Library', 'ics-calendar-pro'),
						'menu_slug' => 'r34icspro-illustrations-library',
						'capability' => get_option('r34ics_admin_access', 'edit_others_posts'),
						'position' => 2,
						'parent_slug' => 'edit.php?post_type=r34icspro_calendar',
						'update_button' => __('Save Changes', 'ics-calendar-pro'),
						'updated_message' => __('Changes saved.', 'ics-calendar-pro'),
					));
				}
				// Make the link even if acf_add_options_page() function doesn't exist
				// This condition occurs when ACF (free) is active on the site
				else {
					add_submenu_page(
						'edit.php?post_type=r34icspro_calendar',
						'ICS Calendar Pro ' . __('Illustrations Library', 'ics-calendar-pro'),
						__('Illustrations Library', 'ics-calendar-pro'),
						get_option('r34ics_admin_access', 'edit_others_posts'),
						'edit.php?post_type=r34icspro_calendar&page=r34icspro-illustrations-library',
						'',
						2
					);
				}
				add_action('acf/input/admin_head', function() {
					// Support sidebar
					if (acf_is_screen('r34icspro_calendar_page_r34icspro-illustrations-library')) {
						add_meta_box(
							'r34icspro-support',
							'ICS Calendar Pro', // We don't want to translate this
							array(&$this, 'admin_support_sidebar'),
							array('acf_options_page'),
							'side',
							'low'
						);
					}
				});
			}
			
			// Settings & Help page
			add_submenu_page(
				'edit.php?post_type=r34icspro_calendar',
				'ICS Calendar Pro ' . __('Settings &amp; Help', 'ics-calendar-pro'),
				__('Settings &amp; Help', 'ics-calendar-pro'),
				get_option('r34ics_admin_access', 'edit_others_posts'),
				'ics-calendar-pro-settings',
				array(&$this, 'admin_settings'),
				99
			);
			
		}
		
		
		public function admin_pro_notices() {
			global $r34ics_embedded;
			$current_screen = function_exists('get_current_screen') ? get_current_screen() : null;
		
			// Dashboard-only notices for administrator-level users
			if (
				function_exists('r34ics_admin_full_access') && 
				r34ics_admin_full_access() &&
				(
					in_array($current_screen->base, array(
						'dashboard',
						'options-general',
						'toplevel_page_ics-calendar',
						'r34icspro_calendar_page_ics-calendar-pro-settings', // Pro version only
					)) ||
					strpos($current_screen->post_type, 'r34ics') !== false // Pro version only
				)
			) {
			
				// Prepare deferred admin notices
				global $r34ics_deferred_admin_notices;
				if (empty($r34ics_deferred_admin_notices)) {
					$r34ics_deferred_admin_notices = get_option('r34ics_deferred_admin_notices', array());
				}
		
				/**
				 * ICS Calendar (free) is installed but is no longer needed
				 *
				 * Note: We deliberately do NOT check r34ics_admin_full_access() here!
				 * (That's because the function may not exist yet immediately after 6.0.0 update.)
				 * It needs to run no matter what, but it should only be triggered by an action
				 * requiring the manage_options capability anyway: installing ICS Calendar Pro,
				 * or trying to reactivate ICS Calendar (free) while Pro is active.
				 */
				if (empty($r34ics_embedded)) {
					deactivate_plugins('ics-calendar/ics-calendar.php');
					$r34ics_deferred_admin_notices['r34icspro_free_no_longer_needed'] = array(
						'content' => '<p>' .
						sprintf(esc_html__('%1$s includes an embedded copy of the latest version of %2$s, so the separate %2$s plugin is no longer needed. %3$s We have automatically deactivated %2$s and it can be safely deleted.', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>', '<strong>ICS Calendar</strong>', '<br /><br />') .
						'</p>',
						'status' => 'info',
						'dismissible' => 'forever',
					);
				}
				
				// Localhost on non-standard port and ICS Events is active
				if (
					get_option('r34icspro_enable_event_cpt') &&
					(
						wp_kses(wp_unslash($_SERVER['SERVER_NAME'] ?? ''), 'data') == 'localhost' ||
						in_array(wp_kses($_SERVER['SERVER_ADDR'] ?? '', 'data'), array('127.0.0.1', '::1'))
					) &&
					!in_array(intval($_SERVER['SERVER_PORT'] ?? 0), array('80', '443'))
				) {
					$r34ics_deferred_admin_notices['r34icspro_localhost_nonstandard_port_ics_events_on'] = array(
						'content' => '<p>' .
						sprintf(esc_html__('You appear to be running %1$s in a local hosting environment on a non-standard port (%2$s), with ICS Events enabled. %3$s If your local environment is running in a virtualized container (e.g. Docker), please be sure you have configured the web server (e.g. Apache, nginx) within the virtual environment to listen on port %4$s. ICS Events makes HTTP requests back to itself, via the WordPress REST API. Virtualization often uses port forwarding, so the "external" port may not be the same as the "internal" port within the virtual environment, and the web server may only be listening on default ports (80 and 443) "inside" the container.', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>', '<strong>' . intval($_SERVER['SERVER_PORT'] ?? 0) . '</strong>', '<br /><br />', '<strong>' . intval($_SERVER['SERVER_PORT'] ?? 0) . '</strong>') .
						'</p>',
						'status' => 'warning',
						'dismissible' => 'forever',
					);
				}

				/**
				 * VERSION-SPECIFIC NOTICES
				 * These were moved here in v.6.0 because their previous placement within the
				 * r34ics_install() and r34ics_update() functions caused them to fail to display
				 * due to activation redirects to the Getting Started page.
				 * (They also wouldn't display if the plugin was auto-updated.)
				 */
				$version = get_option('r34icspro_version');
				$previous_version = get_option('r34icspro_previous_version');

				// Special admin notice for version 4.0
				if (!empty($previous_version) && version_compare($previous_version, '4.0.0', '<')) {
					$r34ics_deferred_admin_notices['r34icspro_version_4_info'] = array(
						'content' => '<p>' .
						sprintf(__('Thank you for installing %1$s version 4.0! %2$s This version introduces the new "Full" view, which uses the %3$s library to present an integrated view with month, week, day and list tabs. Learn more about the new features of this version on our %4$sblog%5$s, or try %6$screating a new Full view calendar%7$s now!', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>', '<br /><br />', '<a href="https://fullcalendar.io/" target="_blank">FullCalendar.io</a>', '<a href="https://icscalendar.com/ics-calendar-pro-version-4-0-released" target="_blank">', '</a>', '<a href="' . admin_url('post-new.php?post_type=r34icspro_calendar') . '">', '</a>') . '</p>',
						'status' => 'info',
						'dismissible' => 'forever',
					);
				}
				else {
					unset($r34ics_deferred_admin_notices['r34icspro_version_4_info']);
				}
			
				// Special admin notice for version 5.1
				if (!empty($previous_version) && version_compare($previous_version, '5.1.0', '<') && !get_option('r34icspro_enable_event_cpt')) {
					$r34ics_deferred_admin_notices['r34icspro_version_5_1_info'] = array(
						'content' => '<p>' .
						sprintf(__('%1$s now includes %2$sICS Events%3$s, allowing you to create and manage calendar events directly within your site, and integrate those events into your calendars created with either Calendar Builder or the %4$s block. To try it out, go to your %5$sSettings%6$s and turn on the %7$sEnable ICS Events%8$s option.', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>', '<strong>', '</strong>', 'ICS Calendar', '<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#settings') . '">', '</a>', '<strong>', '</strong>') .
						'</p>',
						'status' => 'info',
						'dismissible' => 'forever',
					);
				}
				else {
					unset($r34ics_deferred_admin_notices['r34icspro_version_5_1_info']);
				}
			
				// Version 5.10: Activate ICS Events by default for new installs or updates that had ICS Events off
				if (empty($previous_version) || (version_compare($previous_version, '5.10.0', '<') && !get_option('r34icspro_enable_event_cpt'))) {
					// Turn on ICS Events
					update_option('r34icspro_enable_event_cpt', true, true);
					// We need to re-register the CPT for the rewrite rules update
					if (post_type_exists('r34icspro_event')) {
						unregister_post_type('r34icspro_event');
					}
					if (class_exists('R34ICSProEvent')) {
						global $R34ICSProEvent;
						if (is_callable(array($R34ICSProEvent, 'custom_post_type_event'))) {
							$R34ICSProEvent->custom_post_type_event();
						}
					}
					// Display message about ICS Events
					$r34ics_deferred_admin_notices['r34icspro_version_5_10_info'] = array(
						'content' => '<p>' .
						sprintf(__('%1$sICS Events%2$s is now active by default. It is our new way to create and manage calendar events directly within your site, and integrate those events into your calendars created with Calendar Builder. Additional options are available on the %3$sSettings%4$s and page.', 'ics-calendar-pro'), '<a href="' . admin_url('admin.php?page=ics-events-calendar-view') . '"><strong>', '</strong></a>', '<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#settings') . '">', '</a>') .
						'</p>',
						'status' => 'info',
						'dismissible' => 'forever',
					);
				}
				else {
					unset($r34ics_deferred_admin_notices['r34icspro_version_5_10_info']);
				}
			
				// Special admin notice for version 5.10.8
				if (!empty($previous_version) && version_compare($previous_version, '5.10.8', '<') && get_option('r34icspro_enable_event_cpt')) {
					$r34ics_deferred_admin_notices['r34icspro_version_5_10_8_info'] = array(
						'content' => '<p>' .
						sprintf(__('%1$s version 5.10.8 changes ICS Event feed URLs for improved security. If you are using a caching plugin, you may need to clear the cache after this update. If you have the Subscribe Link option turned on for your calendars, note that the subscribe links have changed, and your users will need to re-subscribe using the new link.', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>') .
						'</p>',
						'status' => 'info',
						'dismissible' => 'forever',
					);
				}
				else {
					unset($r34ics_deferred_admin_notices['r34icspro_version_5_10_8_info']);
				}
			
				// Special admin notice for version 6.0
				if (empty($previous_version) || version_compare($previous_version, '6.1', '<')) {
					$r34ics_deferred_admin_notices['r34icspro_version_6_0_announcement'] = array(
						'content' => '<p>' .
						sprintf(__('%1$sWelcome to %2$s version 6!%3$s Version 6.0 introduces the new %4$sAppearance%5$s tab on the %6$sSettings &amp; Help%7$s page with extensive customization options (replaces the previous Customizer tools); the new %8$s Block, which allows you to select a saved calendar in the Block Editor instead of using a shortcode; additional troubleshooting tools; plus a number of performance improvements and interface refinements throughout. This update also lays the groundwork for additional views and other feature enhancements that will be coming in versions 6.1 and 6.2. Please see the %9$schangelog%10$s for more details on this update.', 'ics-calendar-pro'), '<strong style="font-size: 150%; color: var(--r34ics--color--ics-purple);">', '<span style="text-transform: uppercase;">ICS Calendar Pro</span>', '</strong><br /><br />', '<strong>', '</strong>', '<a href="' . esc_url(admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#appearance')) . '">', '</a>', '<strong>ICS Calendar Pro</strong>', '<a href="https://icscalendar.com/pro-changelog/" target="_blank">', '</a>') .
						'</p>',
						'status' => 'info',
						'dismissible' => 'forever',
					);
				}
				else {
					unset($r34ics_deferred_admin_notices['r34icspro_version_6_0_announcement']);
				}
				// Don't show the corresponding free version announcement
				unset($r34ics_deferred_admin_notices['r34ics_version_12_0_announcement']);


				// Save deferred admin notices
				update_option('r34ics_deferred_admin_notices', $r34ics_deferred_admin_notices, false);
			
			}
		}


		public function admin_pro_plugin_action_links($links) {
			if (function_exists('r34ics_admin_full_access') && r34ics_admin_full_access()) {
				array_unshift(
					$links,
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#getting-started') . '" style="white-space: nowrap;">' . __('Getting Started', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#settings') . '">' . __('Settings', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#appearance') . '">' . __('Appearance', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#utilities') . '">' . __('Utilities', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#system-report') . '" style="white-space: nowrap;">' . __('System Report', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#license') . '">' . __('License', 'ics-calendar-pro') . '</a>',
					'<a href="https://icscalendar.com/support/" target="_blank">' . __('Support', 'ics-calendar-pro') . '</a>',
				);
			}
			else {
				array_unshift(
					$links,
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#getting-started') . '">' . __('Getting Started', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#utilities') . '">' . __('Utilities', 'ics-calendar-pro') . '</a>',
					'<a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#system-report') . '">' . __('System Report', 'ics-calendar-pro') . '</a>',
					'<a href="https://icscalendar.com/support/" target="_blank">' . __('Support', 'ics-calendar-pro') . '</a>',
				);
			}
			return $links;
		}
		
		
		public function admin_pro_plugin_row_meta($plugin_meta, $plugin_file, $plugin_data, $status) {
			if ($plugin_file == 'ics-calendar-pro/ics-calendar-pro.php') {
				if ($this->_beta_updates) {
					echo '<p class="r34ics"><mark class="info">';
					printf(esc_html__('%1$s is currently configured to receive beta updates. %2$s Turn off the %3$sReceive beta updates%4$s option on the %5$sSettings page%6$s to receive final release updates only.', 'ics-calendar-pro'), 'ICS Calendar Pro', '<br />', '<strong>', '</strong>', '<a href="' . esc_url(admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#settings')) . '">', '</a>');
					echo '</mark></p>';
				}
				if (r34icspro_ltwc()) {
					echo '<p class="r34ics"><mark class="alert">';
					printf(esc_html__('%1$sImportant:%2$s With manual license activation, ICS Calendar Pro cannot automatically check for updates. %3$s Please %4$scheck our website%5$s regularly to see if a new version is available for download.', 'ics-calendar-pro'), '<strong style="color: var(--r34ics--color--ics-red);">', '</strong>', '<br />', '<a href="https://icscalendar.com/my-account/api-downloads/" target="_blank">', '</a>');
					echo '</mark></p>';
				}
			}
			return $plugin_meta;
		}
		
		
		public function admin_settings() {
			global $R34ICSProLicense;
			$ltwc = r34icspro_ltwc();
			
			// Prepare deferred admin notices
			global $r34ics_deferred_admin_notices;
			if (empty($r34ics_deferred_admin_notices)) {
				$r34ics_deferred_admin_notices = get_option('r34ics_deferred_admin_notices', array());
			}
	
			// Run utilities if applicable
			$this->_admin_page_callback_utilities();
			
			// Run Pro utilities if applicable
			$this->_admin_page_callback_pro_utilities();
			
			// URL tester
			$url_tester_result = $this->_admin_page_callback_url_tester();
		
			// User must have Administrator role for license and settings
			if (function_exists('r34ics_admin_full_access') && r34ics_admin_full_access()) {
			
				// Deactivate license
				if (!$ltwc && isset($_POST['r34icspro-deactivate-license-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34icspro-deactivate-license-nonce'])), 'r34icspro')) {
					$R34ICSProLicense->delete_license();
				}
				
				// Verify and activate license
				elseif (!$ltwc && isset($_POST['r34icspro-activate-license-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34icspro-activate-license-nonce'])), 'r34icspro')) {
					$R34ICSProLicense->set_license();
				}
				
				// Regenerate instance
				elseif (!$ltwc && isset($_POST['r34icspro-regenerate-instance-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34icspro-regenerate-instance-nonce'])), 'r34icspro')) {
					update_option('r34icspro_instance', r34icspro_generate_instance(), false);
					// Note: We say 'below' because this message can only appear on the Settings page, License tab
					$r34ics_deferred_admin_notices['r34icspro_regenerate_instance'] = array(
						'content' => '<p>' .
						sprintf(esc_html__('%1$s instance regenerated. You will need to re-enter your license information below.', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>') .
						'</p>',
						'status' => 'info',
						'dismissible' => false,
					);
					unset($r34ics_deferred_admin_notices['r34icspro_unlicensed']);
					// Delete old license so user will be prompted to re-enter license info
					$R34ICSProLicense->delete_license();
				}
				
				// Save admin settings
				elseif (isset($_POST['r34ics-settings-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34ics-settings-nonce'])), 'r34ics')) {
					$success = true;
					
					// FREE SETTINGS
					if (method_exists('R34ICS', '_admin_page_callback_save_settings')) {
						$this->_admin_page_callback_save_settings();
					}
					else {
						$success = false;
					}
					
					// PRO SETTINGS
					$this->_admin_page_callback_pro_save_settings();
				
					// Success notice is generated by free version, not needed again here
					if (!$success) {
						$r34ics_deferred_admin_notices['r34icspro_settings_error'] = array(
							'content' => '<p>' .
							sprintf(esc_html__('%1$sError: Some settings were not saved.%2$s Please verify you are running the latest versions of the %3$s and %4$s plugins.', 'ics-calendar-pro'), '<mark class="error">', '</mark>', '<strong>ICS Calendar</strong>', '<strong>ICS Calendar Pro</strong>') .
							'</p>',
							'status' => 'error',
							'dismissible' => false,
						);
					}
				}

				// Save admin appearance
				elseif (isset($_POST['r34ics-appearance-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34ics-appearance-nonce'])), 'r34ics')) {
					$success = true;
					
					// FREE APPEARANCE
					if (method_exists('R34ICS', '_admin_page_callback_save_appearance')) {
						$this->_admin_page_callback_save_appearance();
					}
					else {
						$success = false;
					}
					
					// PRO APPEARANCE
					$this->_admin_page_callback_pro_save_appearance();
				
					// Success notice is generated by free version, not needed again here
					if (!$success) {
						$r34ics_deferred_admin_notices['r34icspro_settings_error'] = array(
							'content' => '<p>' .
							sprintf(esc_html__('%1$sError: Some settings were not saved.%2$s Please verify you are running the latest versions of the %3$s and %4$s plugins.', 'ics-calendar-pro'), '<mark class="error">', '</mark>', '<strong>ICS Calendar</strong>', '<strong>ICS Calendar Pro</strong>') .
							'</p>',
							'status' => 'error',
							'dismissible' => false,
						);
					}
				}
				
			}

			// Save deferred admin notices
			update_option('r34ics_deferred_admin_notices', $r34ics_deferred_admin_notices, false);
			
			// @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
			
			// Render template
			include(plugin_dir_path(dirname(__FILE__)) . 'templates/admin/admin.php');

		}
	
	
		public function admin_shortcode_display($post=null) {
			$id = is_object($post) && isset($post->ID) ? $post->ID : intval($post);
			if (!empty($id)) {
				?>
				<div><input type="text" readonly="readonly" onclick="this.select();" value="[ics_calendar id=&quot;<?php echo intval($id); ?>&quot;]" style="min-width: 200px;" /><span class="icon copy button"><span class="dashicons dashicons-admin-page" title="<?php esc_attr_e('Copy to clipboard...', 'ics-calendar-pro'); ?>"></span></span></div>
				<?php
			}
		}
		
		
		public function admin_shortcode_metabox($post=null) {
			$this->admin_shortcode_display($post);
			?>
			<p class="description"><?php
			printf(esc_html__('Copy and paste this shortcode into your content wherever you would like this calendar to display, or use the %1$s block to select this calendar in the Block Editor.', 'ics-calendar-pro'), '<strong>ICS Calendar Pro</strong>');
			?></p>
			<?php
		}
		
		
		public function admin_support_sidebar() {
			if (file_exists($this->admin_sidebar_path)) {
				include_once($this->admin_sidebar_path);
			}
		}
		
		
		public function ajax_js_args($js_args, $args) {
			if (empty($js_args)) { $js_args = array(); }
			if (!empty($args['use_client_tz'])) {
				$js_args['use_client_tz'] = $args['use_client_tz'];
			}
			return $js_args;
		}
		
		
		public function apply_calendar_builder_filters($args) {
			if (empty($args['filters'])) { return; }
			
			global $r34icspro_filters, $r34icspro_filters_guid;
			if (!isset($r34icspro_filters)) { $r34icspro_filters = array(); }

			// Set global guid to match the current shortcode (will change if multiple shortcodes appear on one page)
			$r34icspro_filters_guid = $args['guid'] ?? r34ics_uid();

			// Add shortcode's filters to the global array
			$filters = unserialize(html_entity_decode(rawurldecode(wp_unslash($args['filters'])), ENT_QUOTES));
			$r34icspro_filters[$r34icspro_filters_guid] = $filters;
			
			// Event CSS Classes
			// Only add the filter if it is enabled
			if (!empty($filters['filtereventcssclasses']['enable'])) {
				add_filter('r34ics_event_css_classes', array(&$this, 'apply_calendar_builder_filters_cb_filtereventcssclasses'), 11, 4);
			}
			
			// Event Text Replacement
			// Only add the filter if it is enabled
			if (!empty($filters['filtereventtextreplacement']['enable'])) {
				add_filter('r34ics_display_calendar_event_item', array(&$this, 'apply_calendar_builder_filters_cb_filtereventtextreplacement'), 11, 3);
			}
			
			// Exclude Event
			// Only add the filter if it is enabled
			if (!empty($filters['filterexcludeevent']['enable'])) {
				add_filter('r34ics_display_calendar_exclude_event', array(&$this, 'apply_calendar_builder_filters_cb_filterexcludeevent'), 11, 3);
			}
			
		}
		
		
		public function apply_calendar_builder_filters_cb_filtereventcssclasses($classes, $event, $time, $args) {
			global $r34icspro_filters, $r34icspro_filters_guid;
			if ($args['guid'] == $r34icspro_filters_guid) {
				$filters = $r34icspro_filters[$args['guid']];
				if (!empty($filters['filtereventcssclasses']['rows'])) {
					foreach ((array)$filters['filtereventcssclasses']['rows'] as $row) {
						$match = trim($row['match']);
						$classname = sanitize_title(trim($row['classname'],'.#'));
						if (!empty($match) && !empty($classname)) {
							if (
								(!empty($event['label']) && stripos($event['label'], $match) !== false) ||
								(!empty($event['title']) && stripos($event['title'], $match) !== false) ||
								(!empty($event['description']) && stripos($event['description'], $match) !== false) ||
								(!empty($event['extendedProps']['event_description_html']) && stripos($event['extendedProps']['event_description_html'], $match) !== false)
							) {
								$classes[] = $classname;
							}
						}
					}
				}
			}
			return $classes;
		}


		public function apply_calendar_builder_filters_cb_filtereventtextreplacement($event_item, $event, $args) {
			global $r34icspro_filters, $r34icspro_filters_guid;
			if ($args['guid'] == $r34icspro_filters_guid) {
				$filters = $r34icspro_filters[$args['guid']];
				if (!empty($filters['filtereventtextreplacement']['rows'])) {
					foreach ((array)$filters['filtereventtextreplacement']['rows'] as $row) {
						$match = trim($row['match']);
						$replace = trim($row['replace']);
						if (!empty($match) && !empty($replace)) {
							if ($args['view'] == 'full') {
								if (isset($event_item['title']) && stripos($event_item['title'], $match) !== false) {
									$event_item['title'] = str_ireplace($match, $replace, $event_item['title']);
								}	
								if (isset($event_item['extendedProps']['event_description_html']) && stripos($event_item['extendedProps']['event_description_html'], $match) !== false) {
									$event_item['extendedProps']['event_description_html'] = str_ireplace($match, $replace, $event_item['extendedProps']['event_description_html']);
								}
							}
							else {
								if (isset($event_item['label']) && stripos($event_item['label'], $match) !== false) {
									$event_item['label'] = str_ireplace($match, $replace, $event_item['label']);
								}
								if (isset($event_item['eventdesc']) && stripos($event_item['eventdesc'], $match) !== false) {
									$event_item['eventdesc'] = str_ireplace($match, $replace, $event_item['eventdesc']);
								}
							}
						}
					}
				}
			}
			return $event_item;
		}


		public function apply_calendar_builder_filters_cb_filterexcludeevent($exclude, $event, $args) {
			global $r34icspro_filters, $r34icspro_filters_guid;
			if ($args['guid'] == $r34icspro_filters_guid) {
				$filters = $r34icspro_filters[$args['guid']];
				if (!empty($filters['filterexcludeevent']['rows'])) {
					$match = null;
					foreach ((array)$filters['filterexcludeevent']['rows'] as $row) {
						$exclusion = trim($row['exclusion']);
						if (!empty($exclusion)) {
							$match =	((!empty($event->summary) && stripos($event->summary, $exclusion) !== false) ||
												(!empty($event->description) && stripos($event->description, $exclusion) !== false));
						}
						if ($match) { break; }
					}
					// Include ONLY matching events
					if ($filters['filterexcludeevent']['inclexcl'] == 'include') {
						$exclude = empty($match) ? true : $exclude;
					}
					// Exclude matching events
					else {
						$exclude = !empty($match) ? true : $exclude;
					}
				}
			}
			return $exclude;
		}


		public function calendar_classes($ics_calendar_classes, $args, $implode=false) {
			// Prepare class array
			if (empty($ics_calendar_classes)) { $ics_calendar_classes = array(); }
			elseif (!is_array($ics_calendar_classes)) { $ics_calendar_classes = explode(' ', $ics_calendar_classes); }
			// Conditional CSS classes
			if ($args['view'] == 'basic' && !empty($args['basic_style'])) {
				$ics_calendar_classes[] = 'basic_style_' . sanitize_title($args['basic_style']);
			}
			if ($args['view'] == 'month' && !empty($args['month_list_all'])) {
				$ics_calendar_classes[] = 'month_list_all';
			}
			if ($args['view'] == 'month-with-sidebar' || $args['view'] == 'year-with-sidebar') {
				if (!empty($args['no_sidebar_scroll'])) { $ics_calendar_classes[] = 'no_sidebar_scroll'; }
			}
			if ($args['view'] == 'widget-availability' || $args['view'] == 'year-availability') {
				if (!empty($args['combined_availability'])) { $ics_calendar_classes[] = 'combined_availability'; }
				if (!empty($args['no_angled_backgrounds'])) { $ics_calendar_classes[] = 'no_angled_backgrounds'; }
			}
			if ($args['view'] == 'full') {
				// The fc_tab_mobile_default_list setting was removed in v. 6.0 but may still be in saved calendars
				if (!empty($args['fc_tab_mobile_default_list'])) { $ics_calendar_classes[] = 'fc_tab_mobile_default_list'; }
				if (!empty($args['fc_tab_default_mobile'])) { $ics_calendar_classes[] = 'fc_tab_mobile_default_' . esc_attr($args['fc_tab_default_mobile']); }
			}
			if (in_array($args['view'], (array)$this->list_style_views)) {
				if (!empty($args['pagination_auto_cycle'])) { $ics_calendar_classes[] = 'pagination_auto_cycle'; }
			}
			// Return the CSS classes as a string or an array
			if (!empty($implode)) {
				return implode(' ', $ics_calendar_classes);
			}
			return $ics_calendar_classes;
		}
		
		
		public function calendar_view($view=null, $atts=array()) {
			if (empty($view) && !empty($atts['id'])) {
				$saved_params = r34icspro_get_saved_params($atts['id']);
				if (isset($saved_params['view'])) {
					$view = implode('', (array)$saved_params['view']);
				}
			}
			return $view;
		}
		
		
		public function color_key_html_after_feed_title($feed_key, $args, $ics_data) {
			if (!empty($args['tzinlegend'])) {
				$url_tz = r34ics_get_feed_tz($ics_data, $feed_key);
				echo '&nbsp;&nbsp;<small>' . wp_kses_post(r34icspro_clean_tz_display($url_tz->getName())) . '</small>';
			}
			if (!empty($args['subscribelink']) && is_array($ics_data['urls']) && $feed_url = $ics_data['urls'][$feed_key]) {
				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				echo '&nbsp;&nbsp;' . $this->feed_subscribe_link($feed_url, '', $args['subscribelink']);
			}
			if (!empty($args['hiddenonload_array']) && !empty($args['hiddenonload_array'][$feed_key])) {
				echo '<span data-hiddenonload="1" aria-hidden="true">&nbsp;</span>';
			}
		}
		
		
		public function color_palette_init() {
			// i18n
			// Tranlate color palette labels
			$this->palette_labels_i18n = array(
				'neutral' => __('neutral', 'ics-calendar-pro'),
				'cool' => __('cool', 'ics-calendar-pro'),
				'warm' => __('warm', 'ics-calendar-pro'),
				'ICS Calendar Pro' => 'ICS Calendar Pro', // Don't translate
				'boardroom' => __('board room', 'ics-calendar-pro'),
				'wheat field' => __('wheat field', 'ics-calendar-pro'),
				'rose garden' => __('rose garden', 'ics-calendar-pro'),
				'larch forest' => __('larch forest', 'ics-calendar-pro'),
				'sunrise' => __('sunrise', 'ics-calendar-pro'),
				'midday' => __('midday', 'ics-calendar-pro'),
				'sunset' => __('sunset', 'ics-calendar-pro'),
				'midnight' => __('midnight', 'ics-calendar-pro'),
				'spring' => __('spring', 'ics-calendar-pro'),
				'summer' => __('summer', 'ics-calendar-pro'),
				'autumn' => __('autumn', 'ics-calendar-pro'),
				'winter' => __('winter', 'ics-calendar-pro'),
				'custom' => __('custom palette', 'ics-calendar-pro'),
			);
			
			// Set custom palette values
			$css_vars = get_theme_mod('r34icspro_css_var');
			$prefix = '--r34ics--color--';
			foreach (array_keys($this->palettes['ICS Calendar Pro'] ?? array()) as $key) {
				$this->palettes['custom'][$key] = !empty($css_vars[$prefix . $key]) ? $css_vars[$prefix . $key] : $this->palettes['ICS Calendar Pro'][$key];
			}
		}
		
		
		public function custom_post_type() {
		
			// Custom capability for accessing this CPT
			$cap = get_option('r34ics_admin_access', 'edit_others_posts');
		
			register_post_type('r34icspro_calendar', array(
				'labels' =>	array(
					'name'               => __('Calendar Builder', 'ics-calendar-pro'),
					'singular_name'      => __('Calendar Builder', 'ics-calendar-pro'),
					'add_new_item'       => __('Add New Calendar', 'ics-calendar-pro'),
					'add_new'            => __('Add New Calendar', 'ics-calendar-pro'),
					'all_items'          => __('Calendar Builder', 'ics-calendar-pro'),
					'edit_item'          => __('Edit Calendar', 'ics-calendar-pro'),
					'menu_name'          => 'ICS Calendar', // We don't want to translate this
					'name_admin_bar'     => 'ICS Calendar', // We don't want to translate this
					'new_item'           => __('New Calendar', 'ics-calendar-pro'),
					'not_found_in_trash' => __('No calendars found in Trash.', 'ics-calendar-pro'),
					'not_found'          => __('No calendars found.', 'ics-calendar-pro'),
					'parent_item_colon'  => false,
					'search_items'       => __('Search Calendars', 'ics-calendar-pro'),
					'view_item'          => __('View Calendar', 'ics-calendar-pro'),
				),
				'capability_type' => 'post',
				'capabilities' => array(
					'delete_post' => $cap,
					'delete_posts' => $cap,
					'edit_post' => $cap,
					'edit_posts' => $cap,
				),
				'description' => sprintf(esc_html__('Calendar Builder for %1$s', 'ics-calendar-pro'), 'ICS Calendar Pro'),
				'has_archive' => false,
				'hierarchical' => false,
				'menu_icon' => $this->icon_logo,
				'menu_position' => 49.1,
				'public' => false,
				'show_in_rest' => true,
				'show_ui' => true,
				'supports' => array('title'),
			));
			
			// Custom admin column to display shortcodes
			add_filter('manage_r34icspro_calendar_posts_columns', function($d) {
				$offset = 2;
				$d1 =	array_slice($d, 0, $offset, true) +
						array('r34icspro_admin_columns_shortcode' => __('Shortcode', 'ics-calendar-pro')) +
						array('r34icspro_admin_columns_view' => __('View', 'ics-calendar-pro')) +
						array('r34icspro_admin_columns_feeds' => __('Feeds', 'ics-calendar-pro')) +
						array_slice($d, $offset, null, true);
				return $d1;
			});
			add_action('manage_r34icspro_calendar_posts_custom_column', function ($c, $id) {
				if ($c == 'r34icspro_admin_columns_shortcode') {
					$this->admin_shortcode_display($id);
				}
				if ($c == 'r34icspro_admin_columns_view') {
					if (get_post_meta($id, 'view', true) == 'grid') {
						echo '<div class="r34ics"><mark class="error">' . esc_html__('Grid', 'ics-calendar-pro') . '<br /><small><em>' . esc_html__('Grid view is no longer supported. This calendar will display using Full view, unless edited to use a different view.', 'ics-calendar-pro') . '</em></small></mark></div>';
					}
					else {
						// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
						esc_html_e(r34icspro_convert_view_name(get_post_meta($id, 'view', true)), 'ics-calendar-pro');
					}
				}
				if ($c == 'r34icspro_admin_columns_feeds') {
					echo intval(get_post_meta($id, 'feeds', true));
				}
			}, 10, 2);
			
			// Add custom filtering of calendar list by view
			add_action('restrict_manage_posts', function() {
				global $pagenow, $typenow, $R34ICSPro;
				if (is_admin() && $pagenow == 'edit.php' && $typenow == 'r34icspro_calendar') {
					// phpcs:ignore WordPress.Security.NonceVerification.Recommended
					$selected = sanitize_text_field(wp_unslash($_GET['view'] ?? ''))
					?>
					<label for="ics-calendar-pro-filter-by-view" class="screen-reader-text"><?php esc_html_e('Filter by view', 'ics-calendar-pro'); ?></label>
					<select name="view" id="ics-calendar-pro-filter-by-view">
						<option value=""><?php esc_attr_e('All views', 'ics-calendar-pro'); ?></option>
						<?php
						foreach ((array)$R34ICSPro->views as $view) {
							?>
							<option value="<?php echo esc_attr($view); ?>"<?php
							if ($view == $selected) {
								echo ' selected="selected"';
							}
							?>><?php echo wp_kses_post(r34icspro_convert_view_name($view)); ?></option>
							<?php
						}
						?>
					</select>
					<?php
				}
			}, 10, 0);
			add_filter('parse_query', function($query) {
				if (
					is_admin() && $query->is_main_query() &&
					$query->get('post_type') == 'r34icspro_calendar' &&
					// phpcs:ignore WordPress.Security.NonceVerification.Recommended
					($view = sanitize_text_field(wp_unslash($_GET['view'] ?? '')))
				) {
					$query->set('meta_query', array(array(
						'key' => 'view',
						'value' => $view,
					)));
				}
				return $query;
			}, 10, 1);
			
			// Meta boxes for editing screens
			add_action('add_meta_boxes', function() {
				global $post;
				
				// Shortcode
				add_meta_box(
					'r34icspro-shortcode',
					__('Shortcode', 'ics-calendar-pro'),
					array(&$this, 'admin_shortcode_metabox'),
					array('r34icspro_calendar'),
					'normal',
					'high'
				);
				
				// Support sidebar
				add_meta_box(
					'r34icspro-support',
					'ICS Calendar Pro', // We don't want to translate this
					array(&$this, 'admin_support_sidebar'),
					array('r34icspro_calendar'),
					'side',
					'low'
				);
			});
			
			// Convert removed Grid view to Full view
			// Global $r34icspro_editor_grid_converted is because value will already be modified when other fields are loaded
			add_filter('acf/load_value/key=field_5e0b6d6103f5c', function($value) {
				if ($value == 'grid') {
					global $r34icspro_editor_grid_converted;
					$value = 'full';
					$r34icspro_editor_grid_converted = true;
					// Prepare deferred admin notice
					global $r34ics_deferred_admin_notices;
					if (empty($r34ics_deferred_admin_notices)) {
						$r34ics_deferred_admin_notices = get_option('r34ics_deferred_admin_notices', array());
					}
					$r34ics_deferred_admin_notices['r34icspro_grid_converted_' . intval($post_id)] = array(
						'content' => '<p>' . esc_html__('Grid view is no longer supported. This calendar has been switched to Full view.', 'ics-calendar-pro') . '</p>',
						'status' => 'warning',
						'dismissible' => 'forever',
					);
				}
				return $value;
			}, 10, 1);
			
			// Default converted Grid view to week and day tabs
			add_filter('acf/load_value/key=field_63acc0992cb9b', function($value) {
				global $r34icspro_editor_grid_converted;
				if (!empty($r34icspro_editor_grid_converted)) {
					$value = array('timeGridWeek', 'timeGridDay');
				}
				return $value;
			}, 10, 1);
			
			// Default converted Grid view to use week tab as default
			add_filter('acf/load_value/key=field_63ace22144217', function($value) {
				global $r34icspro_editor_grid_converted;
				if (!empty($r34icspro_editor_grid_converted)) {
					$value = array('timeGridWeek');
				}
				return $value;
			}, 10, 1);

		}
		
		
		public function deprecated_feature_check($args=array()) {
			if (!is_admin() || wp_doing_ajax()) { return; }

			// Use global and transient to avoid redundant activity
			global $r34icspro_deprecated;
			if (!empty($r34icspro_deprecated)) { return; } // We already did this
			$transient_name = 'r34icspro_deprecated_feature_check';
			$r34icspro_deprecated = !empty($no_transient) ? null : get_transient($transient_name);

			// Other variables
			extract((array)$args);
			$version = get_option('r34icspro_version'); // Used for pre/post removal logic

			// Check deprecated items
			if (empty($r34icspro_deprecated)) {
				$r34icspro_deprecated = array();
				// Check for saved calendars using Grid view
				if ((get_option('r34icspro_grid_view_present') || $this->_get_grid_view_posts(false))) {
					$r34icspro_deprecated['views'][] = 'grid';
				}
				// Check for ACF Blocks
				if ((get_option('r34icspro_acf_blocks_present') || $this->_get_acf_blocks_posts(false)) && !is_plugin_active('classic-editor/classic-editor.php')) {
					$r34icspro_deprecated['features'][] = 'ACF Blocks';
				}
				// No deprecated items (set value to true, to indicate that we've checked)
				if (count($r34icspro_deprecated) == 0) { $r34icspro_deprecated = true; }
				// Write to transient
				set_transient($transient_name, $r34icspro_deprecated, WEEK_IN_SECONDS);
			}

			// Admin notices for deprecated features
			if (is_array($r34icspro_deprecated)) {
	
				// Prepare deferred admin notices
				global $r34ics_deferred_admin_notices;
				if (empty($r34ics_deferred_admin_notices)) {
					$r34ics_deferred_admin_notices = get_option('r34ics_deferred_admin_notices', array());
				}
	
				// Deprecated views
				if (!empty($r34icspro_deprecated['views'])) {
					foreach ((array)$r34icspro_deprecated['views'] as $view) {
						switch ($view) {
							case 'grid':
								$r34ics_deferred_admin_notices['r34icspro_deprecated_views_grid_ver_6'] = array(
									'content' => '<p><strong style="color: var(--r34ics--color--ics-red);">' .
									sprintf(esc_html__('Grid view has been removed from %1$s as of version 6.0.', 'ics-calendar-pro'), 'ICS Calendar Pro') .
									'</strong><br />' .
									sprintf(esc_html__('Any existing Grid view calendars will now display using Full view with the "week" and "day" tabs, and the "week" tab preselected. You can also %1$sedit your saved calendars%2$s to change them to a different view.', 'ics-calendar-pro'), '<strong><a href="' . esc_url(admin_url('edit.php?post_type=r34icspro_calendar')) . '">', '</a></strong>') . '</p>',
									'status' => 'warning',
									'dismissible' => 7, // Dismiss for one week because calendars should be converted eventually
								);
								break;
							default: break;
						}
					}
				}
				if (!empty($r34icspro_deprecated['features'])) {
					foreach ((array)$r34icspro_deprecated['features'] as $feature) {
						switch ($feature) {
							case 'ACF Blocks':
								$r34ics_deferred_admin_notices['r34icspro_deprecated_features_acf_block_ver_6'] = array(
									'content' => '<p><strong style="color: var(--r34ics--color--ics-red);">' .
									sprintf(esc_html__('The previous %1$s Block functionality has been removed as of version 6.0.', 'ics-calendar-pro'), 'ICS Calendar') .
									'</strong><br />' .
									sprintf(esc_html__('Please use the %1$sConvert %2$s Blocks%3$s utility to convert any existing %4$s Blocks on your site into Calendar Builder saved calendars and update them to the new format.', 'ics-calendar-pro'), '<strong><a href="' . admin_url('edit.php?post_type=r34icspro_calendar&page=ics-calendar-pro-settings#utilities') . '">', 'ICS Calendar', '</a></strong>', 'ICS Calendar') . '</p>',
									'status' => 'warning',
									'dismissible' => 7, // Dismiss for one week because the utility does need to be run eventually
								);
								break;
							default: break;
						}
					}
				}
	
				// Save deferred admin notices
				update_option('r34ics_deferred_admin_notices', $r34ics_deferred_admin_notices, false);
			}
		}
		
		
		public function display_add_calendar_button($display_button) {
			if (function_exists('get_current_screen') && $current_screen = get_current_screen()) {
				if (@$current_screen->post_type == 'r34icspro_event') {
					$display_button = false;
				}
			}
			return $display_button;
		}
		
		
		// Append data to debugging array
		public function debug_array($arr) {
			if (function_exists('r34ics_admin_full_access') && !r34ics_admin_full_access()) { return $arr; }
			
			// Add ICS Calendar Pro settings
			$settings_fields = array(
				'options_r34icspro_event_illustrations',
				'r34icspro_beta_updates',
				'r34icspro_enable_event_cpt',
				'r34icspro_enable_event_cpt_archive',
				'r34icspro_enable_event_cpt_single',
				'r34icspro_event_admin_nonce_bypass_ip_check',
				'r34icspro_event_cpt_archive_view',
				'r34icspro_event_cpt_slug',
				'r34icspro_ics_events_step_minute',
				'r34icspro_ics_events_url_suffix',
				//'r34icspro_instance', /* Included in r34icspro_license */
				'r34icspro_last_update',
				'r34icspro_license',
				'r34icspro_license_debug',
				'r34icspro_max_execution_time',
				'r34icspro_memory_limit',
				'r34icspro_precache',
				'r34icspro_precache_completed',
				'r34icspro_previous_version',
				'r34icspro_reactivate_acf_on_mu_plugins_uninstall',
				'r34icspro_version',
				'widget_r34icspro_widget',
			);
			foreach ((array)$settings_fields as $field) {
				if ($field == 'r34icspro_license') {
					if (r34icspro_ltwc()) {
						$arr['Plugin Settings'][$field] = 'LTWC';
					}
					elseif ($license = get_option($field)) {
						$license = unserialize(base64_decode($license));
						unset($license['key']); // We don't want to transmit the actual key!
						$arr['Plugin Settings'][$field] = implode(' / ', $license);
					}
					else {
						$arr['Plugin Settings'][$field] = 'ERROR';
					}
				}
				else {
					$arr['Plugin Settings'][$field] = get_option($field);
				}
			}
			
			// Add ICS Calendar Pro appearance
			$appearance_fields = array(
				'r34icspro_color_palette',
				'r34icspro_colors_advanced',
				'r34icspro_css_var',
				'r34icspro_font',
				'r34icspro_text_scaling_full',
				'r34icspro_text_scaling_table',
				'r34icspro_text_scaling_list',
				'r34icspro_hyphenation_off',
				'r34icspro_hide_weekends',
				'r34icspro_legend_style',
				'r34icspro_sticky_sidebar_top',
			);
			foreach ((array)$appearance_fields as $field) {
				$arr['Plugin Appearance'][$field] = get_theme_mod($field);
			}
			
			return $arr;
		}


		public function display_calendar_after_render_template($view, $args, $ics_data) {
			return;
		}
		
		
		public function display_calendar_after_wrapper($view, $args, $ics_data) {

			// Subscribe links
			// Note: Don't use wp_kses_post() on $this->feed_subscribe_link() output; required <input> tag will get stripped!
			if (!empty($args['subscribelink']) && (empty($args['legendposition']) || $args['legendposition'] != 'above')) {
				if (is_array($ics_data['urls']) && count((array)$ics_data['urls']) == 1) {
					// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					echo $this->feed_subscribe_link($ics_data['urls'][0], '', $args['subscribelink']) ?: '';
				}
				elseif (!empty($args['url']) && strpos($args['url'], ' ') === false && strpos($args['url'], '|') === false) {
					// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					echo $this->feed_subscribe_link($args['url'], '', $args['subscribelink']) ?: '';
				}
			}

			// Local timezone identifier
			if (!empty($args['tzinlegend']) && !empty($args['tz']) && r34ics_is_valid_tz($args['tz'])) {
				?>
				<div class="ics-calendar-tz-label">
					<?php esc_html_e('Timezone:', 'ics-calendar-pro'); ?>
					<?php echo wp_kses_post(r34icspro_clean_tz_display($args['tz'])); ?>
				</div>
				<?php
			}
			
		}
		
		
		public function display_calendar_args($args, $atts) {
			foreach ((array)$this->_pro_shortcode_defaults as $key => $value) {
				$args[$key] = isset($atts[$key]) ? $atts[$key] : $value;
			}
			
			// Set default maskinfo value if availability_only is set and maskinfo is not
			if (!empty($args['availability_only']) && empty($args['maskinfo'])) {
				$args['maskinfo'] = __('Booked', 'ics-calendar-pro');
			}

			// Retrieve saved arguments and fill in any that have not already been set
			if (!empty($args['id'])) {
				if (
					get_post_type($args['id']) == 'r34icspro_calendar' &&
					get_post_status($args['id']) == 'publish' &&
					$params = r34icspro_get_saved_params($args['id'])
				) {
					$args = $this->display_calendar_args_params_merge($args, $params);
				}
			}
			
			// Convert removed Grid view to Full view
			if ($args['view'] == 'grid' && !empty($this->_convert_grid_to_full)) {
				$args = $this->_convert_grid_to_full($args);
			}
			
			// View-specific default overrides
			// @todo Integrate view-specific defaults in Pro like the free version
			switch ($args['view']) {
				case 'basic':
					// Obviate need to specify `nostyle` (style reset) when `basic_style` is set
					if (empty($args['nostyle'])) { $args['nostyle'] = !empty($args['basic_style']); }
				case 'full':
					// Force tag defaults
					// Retroactively fixes saved calendars (these fields were removed for Full view in v.5.1.2)
					$args['htmltageventdesc'] = 'div';
					$args['htmltageventtitle'] = 'span';
					break;
				case 'widget-availability':
				case 'year-availability':
					// Force `pastdays` to be 0 on widget/year availability views (these views are not useful for past dates)
					if (isset($args['pastdays']) && $args['pastdays'] > 0) {
						$args['pastdays'] = 0;
					}
					break;
				default: break;
			}
								
			// Inline shortcodes may need to have entities decoded
			// Running this on Calendar Builder calendars should have no effect
			if (!empty($args['regex_pattern']) || !empty($args['regex_replacement'])) {
				$args['regex_pattern'] = html_entity_decode($args['regex_pattern']);
				$args['regex_replacement'] = html_entity_decode($args['regex_replacement']);
			}

			// Dynamic timezones
			if ($icstz = get_query_var('icstz')) {
				if (r34ics_is_valid_tz($icstz)) {
					$args['tz'] = $icstz;
				}
			}
			
			return $args;
		}
		
		
		// Remember: This method is ONLY used for saved calendars loaded by the `id` parameter!
		public function display_calendar_args_params_merge($args=array(), $params=array()) {
			global $R34ICS, $R34ICSProEvent;
			
			// Merge defaults
			$defaults = $R34ICS->shortcode_defaults_merge(array_merge((array)$args, (array)$params));
			$pro_defaults = $this->_pro_shortcode_defaults;
			$args = array_merge((array)$defaults, (array)$pro_defaults, (array)$args);
			
			// Build array of arguments we've set
			$args_set = array(); 
			
			/**
			 * Explicitly defined values either non-empty in the defaults, or are assigned an
			 * arbitrary value with logic in R34ICS::shortcode().
			 * This needs to run early for conditional logic that depends on these values!
			 * These cannot be overridden in shortcode!
			 */
			$explicitly_defined_values = array(
				'description',
				'eventdesc',
				'formatmonthyear',
				'guid',
				'htmltagdate',
				'htmltageventdesc',
				'htmltageventtitle',
				'htmltagmonth',
				'htmltagtime',
				'htmltagtitle',
				'limitdays',
				'mapsource',
				'pastdays',
				'startdate',
				'timeformat',
				'title',
				'view',
			);
			foreach ((array)$explicitly_defined_values as $key) {
				if (!empty($params[$key])) {
					$args[$key] = addslashes(wp_kses($params[$key], 'data')); $args_set[] = $key;
				}
			}

			// Set all parameters that are grouped with feed URLs
			$url = array();
			$icsfeedsource = array();
			$color = array();
			$tz = array();
			$feedlabel = array();
			$hiddenonload = array();
			if (isset($params['feeds'])) {
				if (!is_array($params['feeds'])) { $params['feeds'] = unserialize(html_entity_decode($params['feeds'], ENT_QUOTES)); }
				foreach ((array)$params['feeds'] as $feed) {
					// Determine feed URL
					// ICS Events CPT
					if (get_option('r34icspro_enable_event_cpt') && isset($feed['icsfeedsource']) && $feed['icsfeedsource'] == 'ics_events') {
						if	(
									!empty($feed['icseventscategory']) &&
									$category = get_term_by('id', $feed['icseventscategory'], 'r34icspro_event_category')
								)
						{
							$url[] = home_url($R34ICSProEvent->get_feed_uri_base('category') . '/' . $category->slug);
						}
						else {
							$url[] = home_url($R34ICSProEvent->get_feed_uri_base());
						}
					}
					// Standard external feed URL
					else {
						$url[] = $feed['url'] ?? '';
					}
					// Additional feed-specific parameters
					$icsfeedsource[] = !empty($feed['icsfeedsource']) ? $feed['icsfeedsource'] : 'feed_url';
					$color[] = $feed['color'] ?? '';
					$tz[] = !empty($feed['tz']) ? $feed['tz'] : get_option('timezone_string');
					$feedlabel[] = $feed['feedlabel'] ?? '';
					$hiddenonload[] = !empty($feed['hiddenonload']);
				}
			}
			if (empty($args['url'])) { $args['url'] = implode('|', $url); $args_set[] = 'url'; }
			if (empty($args['icsfeedsource'])) { $args['icsfeedsource'] = implode('|', $icsfeedsource); $args_set[] = 'icsfeedsource'; }
			if (empty($args['color'])) { $args['color'] = implode('|', $color); $args_set[] = 'color'; }
			if (empty($args['tz'])) { $args['tz'] = implode('|', $tz); $args_set[] = 'tz'; }
			if (empty($args['feedlabel'])) { $args['feedlabel'] = implode('|', $feedlabel); $args_set[] = 'feedlabel'; }
			if (empty($args['hiddenonload'])) { $args['hiddenonload'] = implode('|', $hiddenonload); $args_set[] = 'hiddenonload'; }
			
			// Customize title and description
			if (!empty($params['hide_calendar_title_and_description'])) {
				$args['title'] = false;
				$args['description'] = false;
			}
			elseif (!empty($params['customize_title_and_description'])) {
				if (empty($args['title'])) { $args['title'] = $params['title']; }
				if (empty($args['description'])) { $args['description'] = $params['description']; }
			}
			else {
				$args['title'] = true;
				$args['description'] = true;
			}
			
			// This value is never set directly by the user
			$args['limitdayscustom'] = !empty($params['limitdays']); $args_set[] = 'limitdayscustom';
			
			// Set ajax=false (Workaround for a default true value)
			if (!empty($params['no_ajax'])) {
				$args['ajax'] = false; $args_set[] = 'ajax';
			}
			
			// Start on date (with backwards compatibility for start date without start_on present)
			if (isset($params['start_on'])) {
				switch ($params['start_on']) {
					case 'pastdays':
						// Allow fallback to view-specific defaults introduced in free plugin v. 10.6
						if (isset($params['pastdays']) && $params['pastdays'] !== '') {
							$args['pastdays'] = $params['pastdays'];
						}
						// Set start date using same rules added in free plugin v. 9.6.4
						// No check for startdate="today" because $params will never have that setting
						$args['startdate'] = (
							(
								empty($params['startdate']) &&
								in_array($params['view'], array('full', 'week')) &&
								$params['limitdays'] > 0 &&
								$params['limitdays'] <= 8
							)
								? r34ics_date('Ymd', null, null, '-' . intval($params['pastdays']) . ' days')
								: ''
						);
						break;
					case 'startdate':
						$args['startdate'] = $params['startdate'];
						$args['pastdays'] = null;
						break;
					default:
						$args['pastdays'] = $args['startdate'] = null;
						break;
				}
			}
			elseif (!empty($params['startdate'])) {
				$args['startdate'] = $params['startdate'];
			}

			// Event descriptions (optional excerpts)
			if (!empty($params['eventdesc'])) {
				$args['eventdesc'] = !empty($params['eventdesc_length']) ? $params['eventdesc_length'] : $params['eventdesc'];
			}
			else {
				$args['eventdesc'] = false;
			}
			
			// Regular expressions
			// We either need $params['regex'] or all of the 3 $args['regex...'] values set
			if (empty($params['regex']) && (empty(trim($args['regex_pattern'] ?? '')) || empty(trim($args['regex_replacement'] ?? '')) || empty($args['regex_scope']))) {
				$args['regex'] = $args['regex_pattern'] = $args['regex_replacement'] = $args['regex_scope'] = null;
			}
			else {
				$args['regex'] = !empty($params['regex']); $args_set[] = 'regex';
				$args['regex_pattern'] = $params['regex_pattern']; $args_set[] = 'regex_pattern';
				$args['regex_replacement'] = $params['regex_replacement']; $args_set[] = 'regex_replacement';
				$args['regex_scope'] = is_array($params['regex_scope']) ? implode('|',$params['regex_scope']) : $params['regex_scope']; $args_set[] = 'regex_scope';
			}
			
			// Reload
			// Impose a minimum reload value unless extended debugging is on
			if (!empty($params['reload'])) {
				$args['reload'] = r34ics_reload_check($params['reload'], ($params['debug'] ?? 0)); $args_set[] = 'reload';
			}
			
			// VIEW-SPECIFIC SETTINGS
			// REMEMBER: These can't be overridden in shortcode if loading from Calendar Builder by `id` value)!
						
			// Full view only
			if ($args['view'] == 'full') {
				if (!empty($params['fc_business_days'])) {
					$args['fc_business_days'] = implode(',',(array)$params['fc_business_days']); $args_set[] = 'fc_business_days';
				}
				if (!empty($params['fc_height'])) {
					$args['fc_height'] = intval($params['fc_height']); $args_set[] = 'fc_height';
				}
				if (!empty($params['fc_tabs'])) {
					$args['fc_tabs'] = implode(',',(array)$params['fc_tabs']); $args_set[] = 'fc_tabs';
				}
				if (!empty($params['fc_tab_default'])) {
					$args['fc_tab_default'] = $params['fc_tab_default']; $args_set[] = 'fc_tab_default';
				}
				if (!empty($params['fc_tabs_mobile'])) {
					$args['fc_tabs_mobile'] = implode(',',(array)$params['fc_tabs_mobile']); $args_set[] = 'fc_tabs_mobile';
				}
				// The fc_tab_mobile_default_list setting was removed in v. 6.0 but may still be in saved calendars
				if (!empty($params['fc_tab_mobile_default_list'])) {
					$args['fc_tab_mobile_default_list'] = $params['fc_tab_mobile_default_list']; $args_set[] = 'fc_tab_mobile_default_list';
				}
				if (!empty($args['fc_tab_default_mobile'])) {
					$args['fc_tab_default_mobile'] = $params['fc_tab_default_mobile']; $args_set[] = 'fc_tab_default_mobile';
				}
				if (!empty($params['fc_timegrid_all_hours'])) {
					$args['fc_timegrid_all_hours'] = r34ics_boolean_check($params['fc_timegrid_all_hours']); $args_set[] = 'fc_timegrid_all_hours';
				}
				if (empty($args['hours'])) {
					if (empty($params['hours_start'])) { $params['hours_start'] = '0000'; }
					if (empty($params['hours_end'])) { $params['hours_end'] = '2359'; }
					// ACF may not be returning the value in "Hi" format like it's supposed to
					$args['hours'] = r34ics_time_format($params['hours_start'], 'Hi') . '-' . r34ics_time_format($params['hours_end'], 'Hi'); $args_set[] = 'hours';
				}
				if (!empty($params['zoom'])) {
					$args['zoom'] = $params['zoom']; $args_set[] = 'zoom';
				}
			}
			else {
				unset($args['fc_business_days']);
				unset($args['fc_height']);
				unset($args['fc_tabs']);
				unset($args['fc_tab_default']);
				unset($args['fc_tabs_mobile']);
				// The fc_tab_mobile_default_list setting was removed in v. 6.0 but may still be in saved calendars
				unset($args['fc_tab_mobile_default_list']);
				unset($args['fc_tab_default_mobile']);
				unset($args['fc_timegrid_all_hours']);
				unset($args['hours']);
				unset($args['zoom']);
			}
			
			// Masonry view only
			if ($args['view'] == 'masonry') {
				if (!empty($params['masonry_grouping'])) {
					$args['masonry_grouping'] = $params['masonry_grouping'];
				}
				if (!in_array($args['masonry_grouping'], array('event', 'date'))) { $args['masonry_grouping'] = 'event'; }
				$args_set[] = 'masonry_grouping';
			}
			
			// Month with Sidebar view only
			if ($args['view'] == 'month-with-sidebar') {
				if (!empty($params['limit_in_table'])) {
					$args['limit_in_table'] = $params['limit_in_table']; $args_set[] = 'limit_in_table';
				}
				if (!empty($params['table_cell_contents'])) {
					$args['table_cell_contents'] = $params['table_cell_contents']; $args_set[] = 'table_cell_contents';
				}
			}
			else {
				unset($args['limit_in_table']);
				unset($args['table_cell_contents']);
			}
			
			// Up Next view only
			if ($args['view'] == 'up-next') {
				$args['pastdays'] = intval($params['pastdays'] ?? 0);
				$args['limitdays'] = intval($params['limitdays'] ?? 30);
			}
			
			// List-type views only
			if (in_array($args['view'], (array)$this->list_style_views)) {
				$args['no_angled_backgrounds'] = r34ics_boolean_check($params['no_angled_backgrounds'] ?? false);
			}
			
			// Deprecated settings
			$args['currentweek'] = false;
			$args['curlopts'] = '';
			$args['legendinline'] = false;
			$args['method'] = '';
			
			// Apply all remaining settings
			foreach ((array)$args as $arg_k => $arg_v) {
				/**
				 * Allow $params to override $args if $arg_v has its default value, unless empty
				 * string. Empty string check allows "false" booleans for checkboxes, but not
				 * blank text inputs.
				 */
				if (
						!in_array($arg_k, $args_set) &&
						(!isset($defaults[$arg_k]) || $arg_v == $defaults[$arg_k]) &&
						isset($params[$arg_k]) &&
						$params[$arg_k] !== ''
				) {
					// wp_kses() takes a string; if we have an array we need to go for a walk...
					if (is_array($params[$arg_k])) {
						array_walk($params[$arg_k], function(&$value) {
							// @todo Test this flattening of further recursions ¯\_(ツ)_/¯
							if (is_array($value)) { $value = implode('|', $value); }
							$value = addslashes(wp_kses($value, 'data'));
						});
						$args[$arg_k] = $params[$arg_k];
					}
					else {
						$args[$arg_k] = addslashes(wp_kses($params[$arg_k], 'data'));
					}
					$args_set[] = $arg_k;
				}
			}
			
			// Additional processing after merging params into args
			if (in_array($args['view'], array('full', 'month-with-sidebar', 'year-with-sidebar'))) {
				$columnlabels = !empty($atts['columnlabels']) ? $atts['columnlabels'] : (!empty($params['columnlabels']) ? $params['columnlabels'] : '');
				$args['columnlabels'] = in_array($columnlabels, array('full','short','min')) ? $columnlabels : 'short';
			}
			switch ($args['view']) {
				case 'month-with-sidebar':
					$monthnav = !empty($atts['monthnav']) ? $atts['monthnav'] : (!empty($params['monthnav']) ? $params['monthnav'] : '');
					$args['monthnav'] = in_array($monthnav, array('arrows','select','both','compact','none')) ? $monthnav : '';
					break;
				case 'year-with-sidebar';
				case 'year-availability';
					$args['monthnav'] = ''; // @todo This workaround won't be needed once each arg is limited to its relevant views!
					break;
				default: break;
			}
			
			return $args;
		}
		
		
		public function display_calendar_before_render_template($view, $args, $ics_data) {
		}
		
		
		public function display_calendar_before_wrapper($view, $args, $ics_data) {
		
			// Controls wrapper (filter and month/list toggle) 
			if (r34ics_boolean_check($args['showfilter']) || r34ics_boolean_check($args['month_table_list_toggle'])) {
				// We need an actual unique ID in case the page has multiple calendars with the same user-defined id attribute!
				$r34ics_uid = r34ics_uid();
				?>
				<div class="ics-calendar-controls-wrapper">
					<?php
					// Show filter
					if (r34ics_boolean_check($args['showfilter'])) {
						?><div class="ics-calendar-filter"><label><?php esc_html_e('Show events matching:', 'ics-calendar-pro'); ?>&nbsp;<input type="text" class="ics-calendar-filter-text" placeholder="<?php esc_attr_e('Search...', 'ics-calendar-pro'); ?>" /></label><input type="reset" /><span class="ics-calendar-filter-status" title="<?php esc_attr_e('Additional events currently hidden by the search filter. Click "Reset" to show all events.', 'ics-calendar-pro'); ?>"></span></div><?php
					}
					// Show table/list toggle
					if ($view == 'month' && r34ics_boolean_check($args['month_table_list_toggle'])) {
						?><div class="ics-calendar-month-table-list-toggle"><span><?php esc_html_e('View as:', 'ics-calendar-pro'); ?></span><label><input type="radio" name="ics-calendar-month-table-list-toggle-<?php echo esc_attr($r34ics_uid); ?>" value="table" checked="checked" /><span class="icon-month"></span><span class="text-month"><?php esc_html_e('month', 'ics-calendar-pro'); ?></span></label><label><input type="radio" name="ics-calendar-month-table-list-toggle-<?php echo esc_attr($r34ics_uid); ?>" value="list" /><span class="icon-list"></span><span class="text-list"><?php esc_html_e('list', 'ics-calendar-pro'); ?></span></label></div><?php
					}
					?>
				</div>
				<?php
			}
			
			// Subscribe links
			// Note: Don't use wp_kses_post() on $this->feed_subscribe_link() output; required <input> tag will get stripped!
			if (!empty($args['subscribelink']) && (!empty($args['legendposition']) && $args['legendposition'] == 'above')) {
				if (is_array($ics_data['urls']) && count((array)$ics_data['urls']) == 1) {
					// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					echo $this->feed_subscribe_link($ics_data['urls'][0], '', $args['subscribelink']) ?: '';
				}
				elseif (!empty($args['url']) && strpos($args['url'], ' ') === false && strpos($args['url'], '|') === false) {
					// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					echo $this->feed_subscribe_link($args['url'], '', $args['subscribelink']) ?: '';
				}
			}

		}
		
		
		public function display_calendar_color_set($color, $args) {
			switch ($args['view']) {
				// This was created for the now-removed Grid view; reserved for possible future use
				default:
					break; // Do nothing
			}
			return $color;
		}
		
		
		public function display_calendar_early_render($early_render, $view) {
			if (in_array($view, array('full', 'up-next'))) { $early_render = true; }
			return $early_render;
		}
		
		
		public function display_calendar_event_item($event_item, $event, $args) {
			return $event_item;
		}
		
		
		public function display_calendar_filter_ics_data($ics_data, $args=null) {
			extract(array_merge($this->shortcode_defaults, $args));
			
			// Modify events list for availability only
			if (!empty($args['availability_only'])) {
				// Loop through each day in the array
				foreach ((array)$ics_data['events'] as $ykey => $year) {
					foreach ((array)$year as $mkey => $month) {
						foreach ((array)$month as $dkey => $day) {
							// This day has all-day event(s)
							if (!empty($day['all-day'][0])) {
								// Remove all time-based events, and all but one all-day event
								$ics_data['events'][$ykey][$mkey][$dkey] = array('all-day' => array($day['all-day'][0]));
							}
							// This day does not have all-day events; flatten the time-based events instead
							else {
								// @todo This isn't right; we need to find the event in each start time with the latest end time, then merge together overlapping events
								foreach ((array)$day as $tkey => $time) {
									$ics_data['events'][$ykey][$mkey][$dkey][$tkey] = array($time[0]);
								}
							}
						}
					}
				}
			}
		
			// Add filler text for empty days
			if ($args['fill_empty_days']) {
			
				// Determine earliest and latest dates
				$earliest = (strlen($ics_data['earliest'] ?? '') == 6) ? $ics_data['earliest'] . ($args['view'] == 'list' ? r34ics_date('d') : '01') : $ics_data['earliest'];
				$latest = (strlen($ics_data['latest'] ?? '') == 6) ? $ics_data['latest'] . '31' : $ics_data['latest']; // '31' is good enough here
				
				// Build filler "event"
				$filler_event = array(
					'all-day' => array(
						0 => array(
							'filler' => true,
							'label' => $args['empty_day_title'],
							'status' => null,
							'dtstart_time' => null,
							'dtend_time' => null,
							'feed_key' => null,
							'eventdesc' => $args['empty_day_description'],
							'location' => null,
							'organizer' => null,
							'url' => null,
							'attach' => null,
							'rrule' => null,
							'uid' => null,
							'recurrence_id' => null,
						),
					),
				);
				
				// Loop through calendar and find empty days
				foreach ((array)$ics_data['events'] as $year => $months) {
					foreach (array_keys((array)$months) as $month) {
						$t = r34ics_date('t', strtotime($month . '/1/' . $year));
						for ($d = 1; $d <= $t; $d++) {
							$d_lz = $d < 10 ? '0' . $d : $d;
							$ymd_str = $year . $month . $d_lz;
							if ($ymd_str < $earliest || $ymd_str > $latest) { continue; }
							if (!isset($ics_data['events'][$year][$month][$d_lz])) {
								$ics_data['events'][$year][$month][$d_lz] = $filler_event;
							}
						}
					}
				}
				
				// Now we need to sort the days again!
				foreach ((array)$ics_data['events'] as $year => $months) {
					foreach (array_keys((array)$months) as $month) {
						ksort($ics_data['events'][$year][$month]);
					}
					ksort($ics_data['events'][$year]);
				}
				ksort($ics_data['events']);
			}

			// Year views -- get number of months in range to determine if we need a year dropdown
			if (strpos($args['view'], 'year') !== false) {
				$year_dropdown_threshold = 18;
				if (!empty($ics_data['events'])) {
					$months_count = 0;
					foreach ((array)$ics_data['events'] as $year => $months) {
						$months_count += count($months);
					}
					if ($months_count <= $year_dropdown_threshold) {
						$ics_data['no_year_dropdown'] = true;
					}
				}
			}
		
			return $ics_data;
		}
		
		
		// Method name includes "_pro" to avoid overwriting method of same name in parent class
		public function display_calendar_ics_data_init_pro($ics_data=array(), $args=array()) {
			
			// Append selected $args nodes to $ics_data
			$ics_data['booked_day_bg_color'] = $args['booked_day_bg_color'] ?? 'var(--r34ics--element--events--accent--background)';
			$ics_data['empty_day_bg_color'] = $args['empty_day_bg_color'] ?? 'transparent';
			// @todo This is a kludge to get free templates to run r34ics_feed_colors_css() function
			if ($ics_data['empty_day_bg_color'] != 'transparent' && empty($ics_data['colors'])) {
				$ics_data['colors'] = true;
			}
			
			return $ics_data;
		}
		
		
		public function display_calendar_preprocess_raw_feed($ics_contents, $range_start='', $range_end='', $args=null) {
			extract(array_merge($this->shortcode_defaults, $args));

			// @todo Add smart caching option to handle changes only on large feeds
			return $ics_contents;
		}
		
		
		public function display_calendar_range_end($range_end='', $args=array()) {
			$range_end = (string)$range_end; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
		
			extract(array_merge($this->shortcode_defaults, $args));
			switch ($view) {

				case 'up-next':
					$range_end = r34ics_date('Y/m/d', null, null, '+' . intval($args['limitdays'] ?? 30) . ' days');
					break;

				case 'year-with-sidebar':
					$range_end = (intval(substr($range_end,0,4))+1) . '/01/01'; // Range needs extra day to account for timezones
					break;
					
				default:
					break;

			}
			return $range_end;
		}
		
		
		public function display_calendar_range_start($range_start='', $args=array()) {
			$range_start = (string)$range_start; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
		
			extract(array_merge($this->shortcode_defaults, $args));
			switch ($view) {
			
				case 'up-next':
					$range_start = r34ics_date('Y/m/d', null, null, '-' . intval($args['pastdays'] ?? 2) . ' days');
					break;

				case 'year-with-sidebar':
					$range_start = (intval(substr($range_start,0,4))-1) . '/12/31'; // Range needs extra day to account for timezones
					break;
				
				default:
					break;

			}
			return $range_start;
		}
		
		
		public function display_calendar_render_template($view, $args, $ics_data) {
			extract(array_merge($this->shortcode_defaults, $args));
					
			switch ($view) {
			
				case 'full':

					// Set the unique ID for this instance of the calendar 
					if (empty($ics_data['guid'])) {
						$ics_data['guid'] = !empty($guid) ? $guid : r34ics_uid();
					}
					
					// Append events data in JSON format for FullCalendar
					$ics_data = $this->_get_ics_json_events($args, $ics_data);

					break;
			
				case 'masonry': break;
				
				case 'up-next':

					// Set the unique ID for this instance of the calendar 
					if (empty($ics_data['guid'])) {
						$ics_data['guid'] = !empty($guid) ? $guid : r34ics_uid();
					}
					
					// Append events data in JSON format for FullCalendar
					$ics_data = $this->_get_ics_json_events($args, $ics_data);
					
					// Filter down the events list to current/upcoming
					$up_next_events = array();
					$today_counts = array();
					$tomorrow_counts = array();
					$future_counts = array();
					if (!empty($ics_data['events'])) {
						foreach (array_keys((array)$ics_data['urls']) as $key) {
							$today_counts[$key] = 0;
							$tomorrow_counts[$key] = 0;
							$future_counts[$key] = 0;
						}
						// Get each feed's events that are happening "today" (so we can roll with the time... remember caching!)
						// Also get each feed's next future event after today
						$today_check = r34ics_date('Y-m-d');
						$tomorrow_check = r34ics_date('Y-m-d', null, null, '+1 day');
						foreach ((array)$ics_data['events'] as $event) {
							// This is a "today" event
							if (strpos($event['start'], $today_check) === 0 || strpos($event['end'], $today_check) === 0) {
								$event['up_next'] = 'today';
								$up_next_events[$event['start'] . '-' . $event['id']] = $event;
								$today_counts[$event['groupId']]++;
							}
							// This is a "tomorrow" event
							elseif (strpos($event['start'], $tomorrow_check) === 0 || strpos($event['end'], $tomorrow_check) === 0) {
								$event['up_next'] = 'tomorrow';
								$up_next_events[$event['start'] . '-' . $event['id']] = $event;
								$tomorrow_counts[$event['groupId']]++;
							}
							// This is a "future" event (only want one per feed!)
							elseif ($future_counts[$event['groupId']] == 0 && substr($event['start'], 0, 10) > $today_check) {
								$event['up_next'] = 'future';
								$up_next_events[$event['start'] . '-' . $event['id']] = $event;
								$future_counts[$event['groupId']]++;
							}
							// Once every feed has a future event, we're done
							$done = true;
							foreach ((array)$future_counts as $count) {
								if ($count == 0) { $done = false; break; }
							}
							if ($done) { break; }
						}
						ksort($up_next_events);
					}
					// Replace full events list with our filtered-down list
					$ics_data['events'] = $up_next_events;

					break;
			
				default: break;
			}

			// Render view (if exists)
			include(locate_template('ics-calendar/calendar-' . $view . '.php') ?: plugin_dir_path(dirname(__FILE__)) . 'templates/calendar-' . $view . '.php');
		}
		
		
		public function display_calendar_set_earliest($earliest='', $view='') {
			$earliest = (string)$earliest; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
		
			switch ($view) {
				case 'full-month':
					$earliest = substr($earliest,0,6) . '01';
					break;
				case 'full';
					break;
				case 'up-next':
					$earliest = r34ics_date('Ymd');
					break;
				default:
					$earliest = substr($earliest,0,6);
					break;
			}
			return $earliest;
		}
			
	
		public function display_calendar_set_first_date($first_date='', $view='', $startdate='', $pastdays='') {
			switch ($view) {
				case 'full':
				case 'masonry':
					if (!empty($startdate) && intval($startdate) > 20000000) {
						$first_date = $startdate;
					}
					elseif (!empty($pastdays)) {
						$first_date = r34ics_date('Ymd', null, null, '-' . intval($pastdays) . ' days');
					}
					else {
						$first_date = r34ics_date('Ymd');
					}
					break;
				case 'up-next':
					$first_date = r34ics_date('Ymd');
					break;
				default:
					if (!empty($startdate) && intval($startdate) > 20000000) {
						$first_date_base = $startdate;
					}
					elseif (!empty($pastdays)) {
						$first_date_base = r34ics_date('Ymd', $first_date, null, '-' . intval($pastdays) . ' days');
					}
					else {
						$first_date_base = null;
					}
					$first_date = r34ics_date('Ymd', r34ics_first_day_of('month', $first_date_base));
					break;
			}
			return $first_date;
		}
		
		
		public function display_calendar_set_latest($latest='', $view='', $first_ts='', $limitdays='', $limitdayscustom=null) {
			$latest = (string)$latest; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
			$view = (string)$view; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
			$first_ts = (string)$first_ts; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
			$limitdays = (string)$limitdays; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
		
			switch ($view) {
				case 'full-month':
					// FullCalendar excludes the last date, so we want the first day of the next month
					$latest_ym = intval(!empty($limitdayscustom)
						? r34ics_date('Ym', $first_ts, null, '+' . intval($limitdays-1) . ' days')
						: substr($latest,0,6));
					// Determine if next month is also next year
					$latest_y = substr($latest_ym,0,4);
					$latest_m = substr($latest_ym,4,2);
					$next_ym = ($latest_m == 12) ? (string)(intval($latest_y) + 1) . '01' : $latest_ym + 1;
					$latest = (string)$next_ym . '01';
					break;
				case 'full':
					// FullCalendar excludes the last date, so we don't subtract 1 like usual
					$latest = !empty($limitdayscustom) ? r34ics_date('Ymd', $first_ts, null, '+' . intval($limitdays) . ' days') : $latest;
					break;
				case 'up-next':
					$latest = r34ics_date('Ymd', null, null, '+' . intval($limitdays ?? 30) . ' days');
					break;
				default:
					$latest = !empty($limitdayscustom) ? r34ics_date('Ym', $first_ts, null, '+' . intval($limitdays-1) . ' days') : substr($latest,0,6);
					break;
			}
			return $latest;
		}
			
	
		public function display_calendar_set_limit_date($limit_date='', $view='', $first_ts='', $limitdays='') {
			switch ($view) {
				case 'full':
					$limit_date = r34ics_date('Ymd', $first_ts, null, '+' . intval($limitdays-1) . ' days');
					break;
				case 'up-next':
					$limit_date = r34ics_date('Ymd', null, null, '+1 month');
					break;
				case 'year-availability':
				case 'year-with-sidebar':
					$limit_date_base = r34ics_date('Y', $first_ts, null, '+' . intval($limitdays-1) . ' days') . '1231';
					$limit_date = r34ics_date('Ymd', r34ics_last_day_of('year', $limit_date_base));
				default:
					$limit_date_base = r34ics_date('Ymd', $first_ts, null, '+' . intval($limitdays-1) . ' days');
					$limit_date = r34ics_date('Ymd', r34ics_last_day_of('month', $limit_date_base));
					break;
			}
			return $limit_date;
		}
		
		
		public function enqueue_pro_scripts() {
			// @todo Conditional enqueuing

			// Bail out here if ICS Calendar is paused and we're on the front end
			if (!is_admin() && get_option('r34ics_paused')) { return; }
					
			// ICS Calendar Pro JS
			wp_enqueue_script('ics-calendar-pro');
			
			// Add inline scripts (dynamic values)
			wp_add_inline_script('ics-calendar-pro', 'var r34icspro_vars = ' . json_encode(array(
				'i18n' => array(
					'Copied' => __('Copied', 'ics-calendar-pro'),
					'Copy URL' => __('Copy URL', 'ics-calendar-pro'),
				),
				'locale' => str_replace('_', '-', get_locale()),
			)) . ';');
			
			// ICS Calendar Pro CSS
			wp_enqueue_style('ics-calendar-pro');

			// Add inline styles (dynamic values)
			global $R34ICSProCSS;
			wp_add_inline_style('ics-calendar-pro', $R34ICSProCSS->get_css());
			
			// FullCalendar
			wp_enqueue_script('r34icspro-fullcalendar');

			// Masonry
			wp_enqueue_script('jquery-masonry'); // Included in WP core!
			
		}
		
		
		// Method name avoids collision with parent class
		public function event_description_html_pro($args, $event, $classes=array(), $has_desc=false) {
			return;
		}

				
		public function event_description_html_filter($descloc_content, $args, $event, $classes=array(), $has_desc=null) {
			// Calendar illustrations
			if (!empty($args['calendar_illustrations']) && $illustration = r34icspro_calendar_illustration_image($event, @$args['calendar_illustrations'], r34icspro_is_google_calendar($args['url']))) {
				$illustration_html = '<div class="ics-calendar-event-image r34icspro-illus"><img src="' . esc_url($illustration) . '" alt="" aria-hidden="true" /></div>';
				// There might be a slicker function for inserting before the final tag but this works
				$descloc_content = trim($descloc_content);
				$last_tag = '</' . esc_attr($args['htmltageventdesc']) . '>';
				$offset = strlen($descloc_content) - strlen($last_tag);
				$descloc_content = substr($descloc_content, 0, $offset) . $illustration_html . $last_tag;
			}
			// Regular expressions
			if (!empty($args['regex']) && in_array('event_description_html', explode('|',$args['regex_scope']))) {
				$descloc_content = preg_replace(($args['regex_pattern'] ?? ''), ($args['regex_replacement'] ?? ''), $descloc_content);
			}
			// a11y: tabindex for Full view
			if ($args['view'] == 'full') {
				$descloc_content = str_replace(' class="descloc', ' tabindex="0" class="descloc', $descloc_content);
			}
			return $descloc_content;
		}
		
		
		public function event_label_html_filter($title_content, $args, $event=null, $classes=array()) {
			if (!empty($args['regex']) && in_array('event_label_html', explode('|',$args['regex_scope']))) {
				$title_content = preg_replace(($args['regex_pattern'] ?? ''), ($args['regex_replacement'] ?? ''), $title_content);
			}
			return $title_content;
		}
		
		
		public function event_sublabel_html_filter($sublabel_content, $args, $event=null, $classes=array()) {
			if (!empty($args['regex']) && in_array('event_sublabel_html', explode('|',$args['regex_scope']))) {
				$sublabel_content = preg_replace(($args['regex_pattern'] ?? ''), ($args['regex_replacement'] ?? ''), $sublabel_content);
			}
			return $sublabel_content;
		}
		
		
		public function eventdesc_include_time_and_title($include, $args, $event=null) {
			if (!empty($event['multiday']) && in_array($args['view'], array('month-with-sidebar', 'widget', 'year-with-sidebar'))) {
				$include = false;
			}
			return $include;
		}
		
		
		public function feed_colors_css($feed_colors_css='', $ics_data=array(), $padding=false, $hover=false) {
			switch (@$ics_data['view']) {
				case 'month':
				case 'week':
				case 'month-with-sidebar':
					ob_start();
					if (!empty($ics_data['colors'])) {
						foreach ($ics_data['colors'] as $feed_key => $color) {
							?>
							:is(.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>.combinemultiday.layout-month:not([data-month-table-list-toggle=list]), .ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>.layout-month-with-sidebar .ics-calendar-main) .event[data-feed-color="<?php echo esc_attr($color['base']); ?>"]:is(.multiday_first, .multiday_middle, .multiday_last) {
								background-color: <?php echo esc_attr($color['base']); ?>;
							}
							:is(.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>.combinemultiday.layout-month[data-month-table-list-toggle=list], .ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>.layout-month-with-sidebar .ics-calendar-sidebar) .event[data-feed-color="<?php echo esc_attr($color['base']); ?>"]:is(.multiday_first, .multiday_middle, .multiday_last) {
								background-color: <?php echo esc_attr($color['highlight']); ?> !important;
							}
							<?php
						}
					}
					if (!empty($ics_data['empty_day_bg_color'])) {
						?>
						.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.empty,
						.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.empty .events {
							background-color: <?php echo esc_attr($ics_data['empty_day_bg_color']); ?> !important;
							<?php
							if ($ics_data['empty_day_bg_color'] != 'transparent') {
								?>
								color: <?php echo esc_attr(r34ics_color_text4bg($ics_data['empty_day_bg_color'])); ?> !important;
								<?php
							}
							?>
						}
						<?php
					}
					$feed_colors_css .= ob_get_clean();
					break;
				case 'widget':
				case 'year-with-sidebar':
					ob_start();
					if (!empty($ics_data['colors'])) {
						foreach ($ics_data['colors'] as $feed_key => $color) {
							?>
							.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.has_events .day[data-feed-color="<?php echo esc_attr($color['base']); ?>"] {
								border: 2px solid <?php echo esc_attr($color['base']); ?>;
								color: <?php echo esc_attr(r34ics_color_text4bg($color['highlight'])); ?> !important;
								padding: 0;
							}
							.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.today.has_events .day[data-feed-color="<?php echo esc_attr($color['base']); ?>"],
							.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.current.has_events .day[data-feed-color="<?php echo esc_attr($color['base']); ?>"] {
								background-color: <?php echo esc_attr($color['base']); ?>;
								color: <?php echo esc_attr(r34ics_color_text4bg($color['base'])); ?> !important;
							}
							<?php
						}
					}
					if (!empty($ics_data['empty_day_bg_color'])) {
						?>
						.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.empty .day {
							background-color: <?php echo esc_attr($ics_data['empty_day_bg_color']); ?>;
							<?php
							if ($ics_data['empty_day_bg_color'] != 'transparent') {
								?>
								color: <?php echo esc_attr(r34ics_color_text4bg($ics_data['empty_day_bg_color'])); ?> !important;
								<?php
							}
							?>
						}
						<?php
					}
					$feed_colors_css .= ob_get_clean();
					break;
				case 'widget-availability':
				case 'year-availability':
					ob_start();
					if (!empty($ics_data['empty_day_bg_color'])) {
						?>
						.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.empty:not(.past) .day {
							background-color: <?php echo esc_attr($ics_data['empty_day_bg_color']); ?>;
							<?php
							if ($ics_data['empty_day_bg_color'] != 'transparent') {
								?>
								color: <?php echo esc_attr(r34ics_color_text4bg($ics_data['empty_day_bg_color'])); ?> !important;
								<?php
							}
							?>
						}
						<?php
					}
					if (!empty($ics_data['booked_day_bg_color'])) {
						?>
						.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.has_events:not(.past) .day {
							background-color: <?php echo esc_attr($ics_data['booked_day_bg_color']); ?>;
							<?php
							if ($ics_data['booked_day_bg_color'] != 'transparent') {
								?>
								color: <?php echo esc_attr(r34ics_color_text4bg($ics_data['booked_day_bg_color'])); ?> !important;
								<?php
							}
							?>
						}
						<?php
					}
					?>
					.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>:not(.no_angled_backgrounds) .ics-calendar-year-availability-grid td.has_events.multiday_first:not(.multiday_last):not(.past) .day {
						background: linear-gradient(-45deg,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 0%,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 45%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 45%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 100%) !important;
					}
					.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>:not(.no_angled_backgrounds) .ics-calendar-year-availability-grid td.has_events.multiday_last:not(.multiday_first):not(.past) .day {
						background: linear-gradient(-45deg,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 0%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 55%,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 55%,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 100%) !important;
					}
					.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>:not(.no_angled_backgrounds) .ics-calendar-year-availability-grid td.has_events.multiday_first.multiday_last:not(.past) .day {
						background: linear-gradient(-45deg,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 0%,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 45%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 45%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 55%,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 55%,<?php echo esc_attr($ics_data['booked_day_bg_color']); ?> 100%) !important;
					}
					<?php
					if (!empty($ics_data['colors'])) {
						foreach ($ics_data['colors'] as $feed_key => $color) {
							?>
							.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?> td.has_events:not(.past) .day[data-feed-color="<?php echo esc_attr($color['base']); ?>"] {
								background-color: <?php echo esc_attr($color['base']); ?>;
								border: none;
								color: <?php echo esc_attr(r34ics_color_text4bg($color['base'])); ?> !important;
							}
							.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>:not(.no_angled_backgrounds) .ics-calendar-year-availability-grid td.has_events.multiday_first:not(.multiday_last):not(.past) .day {
								background: linear-gradient(-45deg,<?php echo esc_attr($color['base']); ?> 0%,<?php echo esc_attr($color['base']); ?> 50%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 50%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 100%) !important;
							}
							.ics-calendar#<?php echo esc_attr($ics_data['guid']); ?>:not(.no_angled_backgrounds) .ics-calendar-year-availability-grid td.has_events.multiday_last:not(.multiday_first):not(.past) .day {
								background: linear-gradient(-45deg,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 0%,<?php echo esc_attr($ics_data['empty_day_bg_color']); ?> 50%,<?php echo esc_attr($color['base']); ?> 50%,<?php echo esc_attr($color['base']); ?> 100%) !important;
							}
							<?php
						}
					}
					$feed_colors_css .= ob_get_clean();
					break;
				default:
					// Do nothing
					break;
			}
			return $feed_colors_css;
		}
		
		
		public function feed_subscribe_link($feed_url='', $feed_name='', $style='') {
			$feed_url = (string)$feed_url; // Avoid PHP 8.1 "Passing null to parameter" deprecation notice
			ob_start();
			if (filter_var($feed_url, FILTER_VALIDATE_URL)) {
				$webcal_url = preg_replace('/http[s]?:\/\//', 'webcal://', $feed_url);
				$feed_links = array(
					__('Apple/Microsoft', 'ics-calendar-pro') => $webcal_url,
					__('Google/Android', 'ics-calendar-pro') => 'https://www.google.com/calendar/render?cid=' . rawurlencode($webcal_url),
					/* As of 2025.11.10 it seems these links are no longer supported, but direct webcal:// links should work... */
					//__('Microsoft Outlook 365', 'ics-calendar-pro') => 'https://outlook.office.com/owa?path=/calendar/action/compose&rru=addsubscription&url=' . rawurlencode($webcal_url) . '&name=' . rawurlencode(wp_strip_all_tags($feed_name)),
					//__('Microsoft Outlook Live', 'ics-calendar-pro') => 'https://outlook.live.com/owa?path=/calendar/action/compose&rru=addsubscription&url=' . rawurlencode($webcal_url) . '&name=' . rawurlencode(wp_strip_all_tags($feed_name)),
				);
				?>
				<div class="r34icspro-subscribe-link-wrapper">
					<div class="r34icspro-subscribe-button">
						<a href="#" class="subscribe button"><?php
						if (in_array($style, array('both', 'icon'))) {
							echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 640" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" width="24" height="24"' . ($style == 'both' ? ' style="margin-right: 0.25em;"' : '') . '><g fill="currentColor"><path d="m537.916 171.008h-156.475v-20c0-16.557-13.443-30-30-30-16.558 0-30 13.443-30 30v20.933c-61.764 7.564-109.668 60.266-109.668 124.067v250c0 68.989 56.011 125 125 125h250c68.99 0 125-56.011 125-125v-250c0-65.236-50.082-118.868-113.857-124.509v-20.491c0-16.557-13.443-30-30-30-16.558 0-30 13.443-30 30zm-278.238 370.849c0 43.684 35.467 79.151 79.152 79.151h241.697c43.685 0 79.151-35.467 79.151-79.151v-220.844h-400z" transform="matrix(.930909 0 0 .930909 -173.869 -112.647)"/></g></svg>';
						}
						if (empty($style) || $style != 'icon') {
							esc_html_e('Subscribe', 'ics-calendar-pro');
						}
						?></a>
					</div>
					<div class="r34icspro-subscribe-floating-menu">
						<ul>
							<?php
							foreach ((array)$feed_links as $link_text => $link_url) {
								?>
								<li><a href="<?php echo esc_url($link_url); ?>" target="_blank" class="offsite-link" rel="noopener noreferrer nofollow noindex"><?php echo wp_kses_post($link_text); ?></a></li>
								<?php
							}
							?>
							<li><a href="<?php echo esc_url($feed_url); ?>" class="r34icspro-subscribe-floating-menu-copy-url"><?php esc_html_e('Copy URL', 'ics-calendar-pro'); ?></a></li>
						</ul>
					</div>
				</div>
				<?php
			}
			return ob_get_clean();
		}
		
		
		public function has_desc($has_desc, $args, $event) {
			if (!empty($args['calendar_illustrations']) && !empty(r34icspro_calendar_illustration_image($event, @$args['calendar_illustrations'], r34icspro_is_google_calendar($args['url'])))) {
				$has_desc = true;
			}
			return $has_desc;
		}
		
		
		public function illustrations_library_keywords($array) {
			global $r34icspro_custom_illustrations;
			// Get the list once and make it global for reuse
			if (empty($r34icspro_custom_illustrations)) {
				if ($options = r34icspro_get_saved_options('r34icspro_event_illustrations', true)) {
					$r34icspro_custom_illustrations = array();
					// Get the image URLs from the IDs
					foreach ((array)$options as $key => $item) {
						if (
							($image_url = wp_get_attachment_url($item['r34icspro_event_illustrations_image'])) &&
							($terms = explode(',', preg_replace('/,[\s]+/', ',', $item['r34icspro_event_illustrations_keywords'])))
						)
						{ $r34icspro_custom_illustrations[$image_url] = $terms; }
					}
				}
			}
			// Add the list to the filtered array
			if (!empty($r34icspro_custom_illustrations)) {
				$array = array_merge($array, $r34icspro_custom_illustrations);
			}
			return $array;
		}
		
		
		public function is_dev_mode() {
			return $this->_dev_mode;
		}


		public function license_debug($limited=false) {
			if (r34icspro_ltwc() || !is_admin() || wp_doing_ajax() || (function_exists('r34ics_admin_full_access') && !r34ics_admin_full_access())) { return false; }
			global $R34ICSProLicense;
			return (is_object($R34ICSProLicense) && method_exists($R34ICSProLicense, 'license_debug')) ? $R34ICSProLicense->license_debug($limited) : false;
		}
		

		public function license_expires() {
			if (r34icspro_ltwc() || !is_admin() || wp_doing_ajax()) { return ''; }
			global $R34ICSProLicense;
			return (is_object($R34ICSProLicense) && method_exists($R34ICSProLicense, 'license_expires')) ? $R34ICSProLicense->license_expires('display') : false;
		}
		
		
		public function licensed() {
			if (r34icspro_ltwc() || !is_admin() || wp_doing_ajax()) { return true; }
			global $R34ICSProLicense;
			return (is_object($R34ICSProLicense) && method_exists($R34ICSProLicense, 'licensed')) ? $R34ICSProLicense->licensed() : false;
		}
		
		
		public function list_style_views($views) {
			$views = array_merge((array)$views, array(
				'masonry',
				'up-next',
			));
			return $views;
		}
		
		
		// Needed for Loco Translate conflict, when locale is changed after plugin is already active
		public function maybe_update_locale() {
			$locale = determine_locale();
			if (get_option('r34icspro_locale') != $locale) {
				if (is_plugin_active('loco-translate/loco.php')) {
					r34icspro_i18n_symlinks();
				}
				update_option('r34icspro_locale', $locale);
			}
		}
		
		
		public function pro_views($views) {
			$views = array_merge((array)$views, $this->_pro_views);
			return $views;
		}
		
		
		public function purge_pro_calendar_transients() {
			// Only allow this function to run from admin
			if (!is_admin()) { return; }
		
			global $wpdb;
		
			// Delete ICS Calendar Pro pre-cache data associated with saved feed URLs
			// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
			// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery
			$wpdb->query($wpdb->prepare("DELETE FROM `" . $wpdb->options . "` WHERE `option_name` LIKE 'r34icspro\_precached\_%'"));
			// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching
			// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery

			// Reset ALL settings (requires potentially redundant nonce check as this can ONLY be run manually from the Utilities page)
			if (isset($_POST['reset_options']) && (isset($_POST['r34ics-purge-calendar-transients-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34ics-purge-calendar-transients-nonce'])), 'r34ics'))) {
				$reset_options = sanitize_text_field(wp_unslash($_POST['reset_options']));
				if ($reset_options == 'all') {
					// Options we should retain even when purging the rest
					$exclusions = array(
						'r34icspro_ics_events_url_suffix', // Unique hashed string for feed URLs
						'r34icspro_instance',
						'r34icspro_last_update',
						'r34icspro_license',
						'r34icspro_previous_version',
						'r34icspro_version',
					);
	
					$options = r34icspro_get_all_options();
					foreach (array_keys((array)$options) as $option) {
						if (!in_array($option, $exclusions) && stripos($option, 'r34icspro_') === 0) { delete_option($option); }
					}
					$mods = get_theme_mods();
					foreach (array_keys((array)$mods) as $mod) {
						if (!in_array($mod, $exclusions) && stripos($mod, 'r34icspro_') === 0) { remove_theme_mod($mod); }
					}
					/**
					 * Note: non-empty default values are restored with the
					 * R34ICSPro::reset_non_empty_defaults() method, called on the
					 * r34ics_reset_non_empty_defaults action
					 */
				}
			}
		}
		
		
		public function precache_feeds() {
			return; // @todo Pre-caching temporarily removed in v. 6.0.0 pending needed performance improvements
			if (get_option('r34icspro_precache')) {
				// Get all saved feed URLs
				if ($urls = get_option('r34ics_feed_urls')) {
					$memory_limit = array(
						r34ics_memory_limit_mb(),
						intval(get_option('r34icspro_memory_limit')),
					);
					// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
					ini_set('memory_limit', max($memory_limit) . 'M');
					$max_execution_time = array(
						intval(ini_get('max_execution_time')),
						intval(get_option('r34icspro_max_execution_time')),
						((count($urls) + 1) * 20), // Minimum allowable for throttling
					);
					// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
					ini_set('max_execution_time', max($max_execution_time));
					foreach ((array)$urls as $url) {
						// Note: Uses transients; only reloads from source if transient has expired
						$this->_url_get_contents($url, 0, true, false, true);
						update_option('r34icspro_precached_' . $url, wp_date('Y-m-d H:i:s'), false);
						sleep(20);
					}
					update_option('r34icspro_precache_completed', wp_date('Y-m-d H:i:s'), false);
				}
			}
		}
		
		
		public function register_blocks() {

			// Note: Array parameter should include any text strings that require translation!
		
			// Block: ics-calendar-pro/shortcode
			register_block_type(plugin_dir_path(dirname(__FILE__)) . 'blocks/shortcode', array(
				'description' => __('Inserts a saved calendar configuration. Use Calendar Builder to edit the configuration.', 'ics-calendar-pro'),
				'keywords' => array(
					'ICS',
					__('agenda', 'ics-calendar-pro'),
					__('availability', 'ics-calendar-pro'),
					__('calendar', 'ics-calendar-pro'),
					__('events', 'ics-calendar-pro'),
					__('schedule', 'ics-calendar-pro'),
				),
				'render_callback' => array(&$this, 'register_blocks_callback_shortcode'),
			));
			
		}
		
		
		public function register_blocks_callback_shortcode($attr=null) {
			ob_start();
			include(plugin_dir_path(dirname(__FILE__)) . 'blocks/shortcode/shortcode.php');
			return trim(ob_get_clean());
		}
				
		
		public function register_pro_scripts() {
			// Dependencies
			$dep_js = array('jquery');
	
			// ICS Calendar Pro JS
			wp_register_script('ics-calendar-pro', plugin_dir_url(dirname(__FILE__)) . 'assets/script-pro.min.js', $dep_js, get_option('r34icspro_version'), true);
			
			// ICS Calendar Pro CSS
			wp_register_style('ics-calendar-pro', plugin_dir_url(dirname(__FILE__)) . 'assets/style-pro.min.css', array('ics-calendar'), get_option('r34icspro_version'));

			// FullCalendar
			wp_register_script('r34icspro-fullcalendar', plugin_dir_url(dirname(__FILE__)) . 'vendors/fullcalendar/index.global.min.js', array('jquery', 'ics-calendar', 'ics-calendar-pro'), '6.1.10', true);
		}
		
		
		// Set/reset non-empty defaults
		// Used on initial installation and by r34ics_purge_calendar_transients() with $reset_all option
		public function reset_non_empty_defaults() {
			set_theme_mod('r34icspro_color_palette', 'custom');
			update_option('r34icspro_enable_event_cpt', true, true);
		}
		
		
		public function shortcode_defaults_merge_pro($defaults) {
			return array_merge($defaults, $this->_pro_shortcode_defaults);
		}
				

		// @todo Standardize array-based pro defaults and remove special case logic here
		public function shortcode_defaults_new_10_6($shortcode_defaults_new_10_6) {
			// Masonry and Up Next views uses the same options as list view
			foreach ((array)$shortcode_defaults_new_10_6 as $key => $value) {
				if (is_array($value)) {
					if (isset($value['list'])) {
						$shortcode_defaults_new_10_6[$key]['masonry'] = $value['list'];
						$shortcode_defaults_new_10_6[$key]['up-next'] = $value['list'];
					}
				}
			}
			return $shortcode_defaults_new_10_6;
		}
		
		
		public function shortcode_feed_array_params($shortcode_feed_array_params) {
			return array_merge($shortcode_feed_array_params, array(
				'hiddenonload',
				'icsfeedsource',
				'icseventscategory',
			));
		}
		
		
		public function system_report_array($report) {
			$report = $this->debug_array($report);
			// Append vendor versions to report
			$report['Vendor Versions'] = r34icspro_get_vendor_versions();
			// Append Calendar Builder data to report
			if ($cb_data = get_posts(array(
				'post_type' => 'r34icspro_calendar',
				'post_status' => 'any',
				'posts_per_page' => -1
			))) {
				$post_fields = array('post_date', 'post_modified', 'post_title', 'post_status');
				foreach ((array)$cb_data as $cb_item) {
					$arr = array();
					foreach ((array)$post_fields as $post_field) {
						$arr[$post_field] = $cb_item->{$post_field};
					}
					if (isset($cb_item->ID)) {
						$meta = get_post_meta($cb_item->ID);
						foreach ((array)$meta as $key => $value) {
							// Skip ACF field keys
							if (strpos($key, '_') === 0) { continue; }
							// Implode arrays to strings
							$value = implode(',', (array)$value);
							if (!empty($value)) {
								// Skip irrelevant values that are specific to a single view
								if ($meta['view'] != 'month-with-sidebar' && (in_array($key, array('limit_in_table', 'table_cell_contents')))) { continue; }
								// Add value to output array
								$arr[$key] = $value;
							}
						}
						ksort($arr);
						$report['Calendar Builder']['id=' . $cb_item->ID] = $arr;
					}
				}
			}
			else {
				$report['Calendar Builder'] = 'N/A';
			}
			// Append Illustrations Library data to report
			if ($il_data = r34icspro_get_saved_options('r34icspro_event_illustrations', true)) {
				$report['Illustrations Library'] = $il_data;
			}
			else {
				$report['Illustrations Library'] = 'N/A';
			}
			return $report;
		}


		// @todo Consider migrating this functionality into class-r34icspro-css.php
		protected function _admin_page_callback_pro_save_appearance() {
			// Yes, this is redundant... intentionally
			if (isset($_POST['r34ics-appearance-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34ics-appearance-nonce'])), 'r34ics')) {
				
				// color_palette and related CSS variables
				set_theme_mod('r34icspro_color_palette', (isset($_POST['color_palette']) ? sanitize_text_field(wp_unslash($_POST['color_palette'])) : 'custom'));
				set_theme_mod('r34icspro_colors_advanced', !empty($_POST['colors_advanced']));
				if (isset($_POST['r34icspro_css_var'])) {
					// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
					$r34icspro_css_var = array_map(function($value) {
						return sanitize_text_field(wp_unslash($value));
					},
					$_POST['r34icspro_css_var']);
					set_theme_mod('r34icspro_css_var', $r34icspro_css_var);
					// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
				}
								
				// font
				set_theme_mod('r34icspro_font', (isset($_POST['font']) ? sanitize_text_field(wp_unslash($_POST['font'])) : ''));
				
				// text_scaling_full
				set_theme_mod('r34icspro_text_scaling_full', (isset($_POST['text_scaling_full']) ? sanitize_text_field(wp_unslash($_POST['text_scaling_full'])) : ''));
				
				// text_scaling_table
				set_theme_mod('r34icspro_text_scaling_table', (isset($_POST['text_scaling_table']) ? sanitize_text_field(wp_unslash($_POST['text_scaling_table'])) : ''));
				// Rename former text_scaling_grid to text_scaling_table
				if (!isset($_POST['text_scaling_table']) && ($text_scaling_grid = get_theme_mod('r34icspro_text_scaling_grid'))) {
					set_theme_mod('r34icspro_text_scaling_table', $text_scaling_grid);
					remove_theme_mod('r34icspro_text_scaling_grid');
				}
				
				// text_scaling_list
				set_theme_mod('r34icspro_text_scaling_list', (isset($_POST['text_scaling_list']) ? sanitize_text_field(wp_unslash($_POST['text_scaling_list'])) : ''));
				
				// hyphenation_off
				set_theme_mod('r34icspro_hyphenation_off', intval(isset($_POST['hyphenation_off']) ? $_POST['hyphenation_off'] : 0));
				
				// hide_weekends
				set_theme_mod('r34icspro_hide_weekends', !empty($_POST['hide_weekends']));
				
				// legend_style
				set_theme_mod('r34icspro_legend_style', intval(isset($_POST['legend_style']) ? $_POST['legend_style'] : 0));
				
				// sticky_sidebar_top
				set_theme_mod('r34icspro_sticky_sidebar_top', (isset($_POST['sticky_sidebar_top']) ? sanitize_text_field(wp_unslash($_POST['sticky_sidebar_top'])) : ''));
			}
		}
		
		
		protected function _admin_page_callback_pro_save_settings() {
			// Yes, this is redundant... intentionally
			if (isset($_POST['r34ics-settings-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34ics-settings-nonce'])), 'r34ics')) {

				// enable_event_cpt
				if (empty($_POST['enable_event_cpt'])) {
					update_option('r34icspro_enable_event_cpt', false, true);
					update_option('r34icspro_ics_events_step_minute', '', false);
					update_option('r34icspro_enable_event_cpt_archive', false, true);
					update_option('r34icspro_event_cpt_archive_view', '', false);
					update_option('r34icspro_enable_event_cpt_single', false, true);
					update_option('r34icspro_event_cpt_slug', '', true);
					update_option('r34icspro_event_admin_nonce_bypass_ip_check', false, false);
					// We need to de-register the CPT for the rewrite rules update
					if (post_type_exists('r34icspro_event')) {
						unregister_post_type('r34icspro_event');
					}
				}
				else {
					update_option('r34icspro_enable_event_cpt', true, true);
					update_option('r34icspro_ics_events_step_minute', intval(isset($_POST['ics_events_step_minute']) ? $_POST['ics_events_step_minute'] : 5), false);
					update_option('r34icspro_enable_event_cpt_archive', !empty($_POST['enable_event_cpt_archive']), true);
					update_option('r34icspro_event_cpt_archive_view', (isset($_POST['event_cpt_archive_view']) && in_array($_POST['event_cpt_archive_view'], (array)$this->views) ? sanitize_text_field(wp_unslash($_POST['event_cpt_archive_view'])) : 'full'), false);
					update_option('r34icspro_enable_event_cpt_single', !empty($_POST['enable_event_cpt_single']), true);
					update_option('r34icspro_event_cpt_slug', (!empty($_POST['event_cpt_slug']) ? sanitize_title(wp_unslash($_POST['event_cpt_slug'])) : 'ics-events'), true);
					update_option('r34icspro_event_admin_nonce_bypass_ip_check', !empty($_POST['event_admin_nonce_bypass_ip_check']), false);
					// We need to re-register the CPT for the rewrite rules update
					if (post_type_exists('r34icspro_event')) {
						unregister_post_type('r34icspro_event');
					}
					if (class_exists('R34ICSProEvent')) {
						global $R34ICSProEvent;
						if (is_callable(array($R34ICSProEvent, 'custom_post_type_event'))) {
							$R34ICSProEvent->custom_post_type_event();
						}
					}
				}
				// Used instead of flush_rewrite_rules() since that doesn't work reliably at this point in the process
				// See: https://developer.wordpress.org/reference/functions/flush_rewrite_rules/#comment-4023
				delete_option('rewrite_rules');
				
				// precache
				update_option('r34icspro_precache', !empty($_POST['precache']), false);
				
				// memory_limit and max_execution_time
				if (!empty($_POST['precache'])) {
					$memory_limit_int = isset($_POST['memory_limit']) ? intval($_POST['memory_limit']) : 256;
					$max_execution_time_int = isset($_POST['max_execution_time']) ? intval($_POST['max_execution_time']) : 30;
					if (!empty($memory_limit_int)) {
						update_option('r34icspro_memory_limit', $memory_limit_int . 'M', false);
					}
					else {
						delete_option('r34icspro_memory_limit');
					}
					/**
					 * Check specifically for an empty value before deleting; allow 0.
					 * If admin clears this value, form will default to the server's
					 * max_execution_time value on reload. The form itself also has a check to
					 * force a non-negative value, because it should always be 0 or greater, but
					 * DreamHost defaults the value to -1, which was causing form validation
					 * errors and preventing changes to the settings from being saved.
					 */ 
					if (isset($_POST['max_execution_time']) && $_POST['max_execution_time'] !== '') {
						update_option('r34icspro_max_execution_time', $max_execution_time_int, false);
					}
					else {
						delete_option('r34icspro_max_execution_time');
					}
				}
				else {
					delete_option('r34icspro_memory_limit');
					delete_option('r34icspro_max_execution_time');
				}
				
				// beta_updates
				update_option('r34icspro_beta_updates', !empty($_POST['beta_updates']), true);
				
				// license_debug
				update_option('r34icspro_license_debug', !empty($_POST['license_debug']), false);
				if (empty($_POST['license_debug'])) {
					delete_option('r34icspro_license_diagnostic_log');
				}
				
				// These options were removed in version 6.0
				delete_option('r34icspro_noblock');
				delete_option('r34icspro_early_acf_init');
				delete_option('r34icspro_show_deprecated');
			
			}
		}
		
		
		// Convert a pre-6.0 Grid calendar to the equivalent Full view
		// Used to make unmodified Grid calendars render; admin should still convert the saved calendar!
		protected function _convert_grid_to_full($args) {
			if (isset($args['view']) && $args['view'] == 'grid') {
				$args['view'] = 'full';
				$args['fc_tab_default'] = 'timeGridWeek';
				$args['fc_tab_default_mobile'] = 'timeGridDay';
				$args['fc_tabs'] = 'timeGridWeek,timeGridDay';
				$args['fc_tabs_mobile'] = 'timeGridDay';
			}
			return $args;
		}
		
		
		protected function _admin_page_callback_pro_utilities() {
			global $r34icspro_acf_blocks_step, $r34icspro_acf_blocks_status, $r34icspro_acf_blocks_affected_posts, $r34ics_deferred_admin_notices;
			
			// Convert ACF Blocks
			if (isset($_POST['r34icspro-convert-acf-blocks-nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['r34icspro-convert-acf-blocks-nonce'])), 'r34icspro')) {
				$r34icspro_acf_blocks_step = intval($_POST['step'] ?? 0);
			
				switch ($r34icspro_acf_blocks_step) {
				
					// Retrieve and display list of affected pages and posts
					case 1:
						$r34icspro_acf_blocks_affected_posts = $this->_get_acf_blocks_posts();
						break;
					
					// Convert affected pages and posts
					case 2:
						// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
						// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
						// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
						$r34icspro_acf_blocks_post_ids = array_filter(filter_var_array(($_POST['affected_posts'] ?? array()), FILTER_SANITIZE_NUMBER_INT));
						// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
						// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
						// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
						if (count($r34icspro_acf_blocks_post_ids) > 0) {
							$r34icspro_acf_blocks_status = array('processed' => array(), 'errors' => array());
							foreach ((array)$r34icspro_acf_blocks_post_ids as $id) {
								$matches = array();
								if ($post = get_post($id)) {
									$post_content = $post->post_content;
									preg_match_all('|<!-- wp:acf/ics-calendar (.+?) /-->|', $post_content, $matches);
									// The list of matching calendar JSON strings will be in $matches[1]
									if (count($matches[1]) > 0) {
										foreach ((array)$matches[1] as $key => $json) {
											// Gather the calendar data
											$calendar = array(
												'key' => intval($id) . '-' . intval($key),
												'title' => __('Converted Block', 'ics-calendar-pro') . ' - ' . $post->post_title . ' (' . intval($id) . (count($matches[1]) > 1 ? '-' . intval($key) : '') . ')',
												'data' => json_decode($json, true, 5, JSON_OBJECT_AS_ARRAY),
											);
											// Is the JSON what we expect? If so, create a new Calendar Builder calendar...
											if (!empty($calendar['data']['data'])) {
												// Create the new Calendar Builder CPT post
												if ($new_calendar_id = wp_insert_post(array(
													'post_title' => trim(wp_strip_all_tags($calendar['title'])),
													'post_status' => 'publish',
													'post_type' => 'r34icspro_calendar',
													'comment_status' => 'closed',
													'ping_status' => 'closed',
												))) {
													// Loop through the ACF meta data and write it to wp_postmeta for this calendar
													foreach ((array)$calendar['data']['data'] as $data_key => $data_val) {
														add_post_meta(intval($new_calendar_id), $data_key, $data_val);
													}
													// Generate the new shortcode block tag
													$shortcode = '<!-- wp:ics-calendar-pro/shortcode {"calendar_id":' . intval($new_calendar_id) . '} /-->';
													// Replace the original block with the new shortcode in the post content
													$post_content = str_replace('<!-- wp:acf/ics-calendar ' . $json . ' /-->', $shortcode, $post_content);
												}
												// An error occurred creating the new calendar post
												else {
													$r34icspro_acf_blocks_status['errors'][] = intval($id) . '-' . intval($key);
												}
											}
											// ...and if not, just delete the block from the post content, since it doesn't contain any data anyway!
											else {
												$post_content = str_replace('<!-- wp:acf/ics-calendar ' . $json . ' /-->', '', $post_content);
											}
											// Save the updated post content
											if ($post_content != $post->post_content) {
												if (wp_update_post(array('ID' => $id, 'post_content' => $post_content), false, false)) {
													$r34icspro_acf_blocks_status['processed'][] = intval($id) . '-' . intval($key);
												}
												else {
													$r34icspro_acf_blocks_status['errors'][] = intval($id) . '-' . intval($key);
												}
											}
										}
									}
								}
							}
						}
						// Reset the status checks
						$this->_get_acf_blocks_posts();
						if (empty($r34ics_deferred_admin_notices)) {
							$r34ics_deferred_admin_notices = get_option('r34ics_deferred_admin_notices', array());
						}
						unset($r34ics_deferred_admin_notices['r34icspro_deprecated_features_acf_block']);
						update_option('r34ics_deferred_admin_notices', $r34ics_deferred_admin_notices, false);
						delete_transient('r34icspro_deprecated_feature_check');
						break;
					
					default: break;
				
				}
			
			}
			
		}
		
		
		protected function _get_acf_blocks_posts($list=true) {
			global $wpdb;
			if ($list || null === get_option('r34icspro_acf_blocks_present', null)) {
				$sql = "SELECT `ID`, `post_title`, `post_type`, `post_status` FROM `" . $wpdb->posts . "` WHERE `post_content` LIKE '%<!-- wp:acf/ics-calendar%' AND `post_status` NOT IN ('inherit', 'trash')";
				// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
				// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
				$acf_blocks_posts = $wpdb->get_results($sql);
				// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter
				// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching
				if (count($acf_blocks_posts) > 0) {
					update_option('r34icspro_acf_blocks_present', true, false);
				}
				else {
					update_option('r34icspro_acf_blocks_present', false, false);
				}
				return $acf_blocks_posts;
			}
			else {
				return get_option('r34icspro_acf_blocks_present');
			}
		}
		
		
		// Used to detect saved calendars that use the removed Grid view; do not remove!
		protected function _get_grid_view_posts($list=true) {
			if ($list || null === get_option('r34icspro_grid_view_present', null)) {
				$grid_posts = get_posts(array(
					// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
					'meta_key' => 'view',
					// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
					'meta_value' => 'grid',
					'post_status' => array('publish', 'draft', 'pending'),
					'post_type' => 'r34icspro_calendar',
					'posts_per_page' => -1,
				));
				if (count($grid_posts) > 0) {
					update_option('r34icspro_grid_view_present', true, false);
				}
				else {
					update_option('r34icspro_grid_view_present', false, false);
				}
				return $grid_posts;
			}
			else {
				return get_option('r34icspro_grid_view_present');
			}
		}
		
		
		protected function _get_ics_json_events($args=array(), $ics_data=array()) {

			// Bail out here if ICS Calendar is paused and we're on the front end
			if (!is_admin() && get_option('r34ics_paused')) { return; }
			
			do_action('r34ics_display_calendar_after_args', $args);

			// Should we use transients?
			$use_transients = intval($args['reload'] ?? '') != 1;

			// Reset debug messages for this call
			$this->debug = $args['debug'];
			if ($this->debug) { $this->debug_messages = array('args' => $args); }
		
			// Adjust memory limit if appropriate
			if ($display_calendar_memory_limit = get_option('r34ics_display_calendar_memory_limit')) {
				// Check against current memory limit
				if (r34ics_memory_limit_mb() < intval($display_calendar_memory_limit)) {
					// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
					ini_set('memory_limit', $display_calendar_memory_limit);
				}
			}
			if ($this->debug) {
				$this->debug_messages['Memory limit (MB)'] = r34ics_memory_limit_mb();
			}
				
			// Adjust time limit if appropriate
			if ($display_calendar_time_limit = get_option('r34ics_display_calendar_time_limit', ini_get('max_execution_time'))) {
				// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
				set_time_limit(intval($display_calendar_time_limit));
			}
			if ($this->debug) {
				$this->debug_messages['Time limit (seconds)'] = intval($display_calendar_time_limit);
			}
			
			// Get ICS data, from transient if possible
			if (method_exists('R34ICS', 'display_calendar_transient_hash')) {
				$transient_name = __METHOD__ . '_' . $this->display_calendar_transient_hash($args);
			}
			// Deprecated; retained only for compatibility with ICS Calendar < v.10.11.1
			else {
				$transient_name = __METHOD__ . '_' . sha1(serialize($args));
			}
			// Note: $ics_data is passed in containing only 'guid'
			if (empty($ics_data['urls']) && $use_transients) {
				$loaded_from_transient = true;
				$ics_data = (array)get_transient($transient_name);
				if (!empty($ics_data) && $this->debug) {
					$this->debug_messages['Loaded from transient'] = size_format(strlen(serialize($ics_data)), 2);
					$this->debug_messages['Transient name'] = $transient_name;
				}
				$ics_data['loaded_from_transient'] = true;
			}

			// No passed or transient ICS data; retrieve and parse ICS data
			if (empty($ics_data['urls'])) {
				$ics_data['loaded_from_transient'] = false;

				// Set basic info parameters
				$ics_data = array_merge($this->display_calendar_ics_data_init($args), (array)$ics_data);
			
				// Determine rough date range for parser
				$ics_data['range'] = $this->display_calendar_date_range($args);
				
				// Default values for display date range
				$first_date = r34ics_date('Ymd');
				$limit_date = r34ics_date('Ymd', $first_date, null, '+' . intval($args['limitdays']-1) . ' days');

				// Set exact display date range
				$first_date = apply_filters('r34ics_display_calendar_set_first_date', $first_date, $args['view'], $args['startdate'], $args['pastdays']);
				$first_ts = strtotime($first_date);
				$limit_date = apply_filters('r34ics_display_calendar_set_limit_date', $limit_date, $args['view'], $first_ts, $args['limitdays']);
				// @todo This is a workaround; modify the input parameters for the filters to support $args
				$earliest_latest_view = (!empty($args['fc_tabs']) && strpos($args['fc_tabs'], 'dayGridMonth') !== false) ? 'full-month' : $args['view'];
				$ics_data['earliest'] = apply_filters('r34ics_display_calendar_set_earliest', $first_date, $earliest_latest_view);
				$ics_data['latest'] = apply_filters('r34ics_display_calendar_set_latest', $limit_date, $earliest_latest_view, $first_ts, $args['limitdays'], $args['limitdayscustom']);
				
				// Debugging information
				if ($this->debug) {
					$this->debug_messages['Included date range'] = $ics_data['range'];
				}
		
				// Process each individual feed URL
				$single_feed = (count((array)$ics_data['urls']) == 1);
				foreach ((array)$ics_data['urls'] as $feed_key => $url) {
			
					// Get timezone for this feed
					$url_tz = r34ics_get_feed_tz($ics_data, $feed_key);
		
					// Parse feed URL
					$ICal = $this->ical_parse_url($url, $url_tz->getName(), $args, $ics_data['range']);

					// No ICS data present -- throw error and move on to the next feed
					if (!is_object($ICal)) {
						// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
						trigger_error(esc_html__('ICS file could not be retrieved or was empty. Please verify URL is correct, and check your php.ini configuration to verify either cURL or allow_url_fopen is available. If you are using spaces to delimit multiple URLs, you may also wish to try using the pipe character instead. Affected URL:', 'ics-calendar-pro') . ' ' . esc_url($url), E_USER_NOTICE);
						continue;
					}
				
					// Update general calendar information
					// Handle 1/true literal values for $title and $description
					if (in_array((string)$args['title'], array('1', 'true'))) {
						$ics_data['title'] = $single_feed ? $ICal->calendarName() : '';
					}
					if (in_array((string)$args['description'], array('1', 'true'))) {
						$ics_data['description'] = $single_feed ? $ICal->calendarDescription() : '';
					}
					$ics_data['timezone'][$url] = $ICal->calendarTimeZone();
					if (is_array($ics_data['feed_titles']) && empty($ics_data['feed_titles'][$feed_key])) {
						$ics_data['feed_titles'][$feed_key] = $ICal->calendarName();
					}
					
					// Debugging information
					if ($this->debug) {
						$this->debug_messages[$url]['Calendar name'] = $ICal->calendarName();
						$this->debug_messages[$url]['Calendar description'] = $ICal->calendarDescription();
						$this->debug_messages[$url]['Calendar time zone'] = $ICal->calendarTimeZone();
						$this->debug_messages[$url]['Default time zone'] = $url_tz->getName();
						if ($ICal->hasEvents() == false) {
							$this->debug_messages[$url]['Errors'][] = 'Calendar contains no events.';
						}
						$current_memory_usage = memory_get_usage();
						if (!isset($this->debug_messages['Peak memory usage']) || $current_memory_usage > $this->debug_messages['Peak memory usage']) {
							$this->debug_messages['Peak memory usage'] = memory_get_usage();
						}
					}

					// Set values for FullCalendar.io properties
					switch ($args['columnlabels']) {
						case 'full': $ics_data['day_header_format'] = 'long'; break;
						case 'short': $ics_data['day_header_format'] = 'short'; break;
						case 'min': $ics_data['day_header_format'] = 'narrow'; break;
						default: $ics_data['day_header_format'] = ''; break;
					}
				
					// Process events
					if ($ics_events = $ICal->eventsFromRange($ics_data['range']['start'], $ics_data['range']['end'])) {
									
						// Assemble events
						foreach ((array)$ics_events as $event_key => $event) {
					
							// Allow external filtering of events
							$exclude_event = apply_filters('r34ics_display_calendar_exclude_event', false, $event, $args);
							if (!empty($exclude_event)) { continue; }

							// 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);
							$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);
								$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 = '';
							}
							
							// Convert $event object to an R34ICS method-compatible array
							$event_item = (array)$event;
							$event_item['attach'] = $this->parse_attach_array($event->attach_array, $args['sametab']);
							$event_item['eventdesc'] = ($event->x_alt_desc ?? ($event->description ?: ''));
							$event_item['feed_key'] = $feed_key;
							$event_item['label'] = ($event->summary ?: '');
							$event_item['tz_start'] = ($args['eventlocaltime'] ? ($event->dtstart_array[0]['TZID'] ?? $url_tz) : $url_tz);
							$event_item['tz_end'] = ($args['eventlocaltime'] ? ($event->dtend_array[0]['TZID'] ?? $url_tz) : $url_tz);

							// 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))
									)
								)
							{
								$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;
								$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,
								);
							}

							// Standard events
							$event_item['dtstart_date'] = empty($all_day) ? $dtstart_date : '';
							$event_item['dtend_date'] = empty($all_day) ? $dtend_date : '';
							$event_item['dtstart_time'] = empty($all_day) ? $dtstart_time : '';
							$event_item['dtend_time'] = empty($all_day) ? $dtend_time : '';
							$event_item['start'] = empty($all_day) ? r34ics_time_format($event_item['dtstart_time'], $args['timeformat'], $event_item['tz_start'], $dtstart_date) : '';
							$event_item['end'] = empty($all_day) ? r34ics_time_format($event_item['dtend_time'], $args['timeformat'], $event_item['tz_end'], $dtend_date) : '';
							
							// Determine CSS classes to include
							$classes = array('event');
							if (empty($args['toggle'])) { $classes[] = 'hover_block'; }
					
							// Build event array in FullCalendar.io format
							// This appears directly in <script> tags in the output HTML so use discretion when including data!
							$fc_event = array(
								'allDay' => $all_day,
								'classNames' => $classes,
								'editable' => false,
								'end' => (
									(in_array($args['extendmultiday'], array('both', 'allday')) && !empty($event_item['multiday']) && empty($event_item['multiday']['end_time']) && ($event_item['multiday']['end_date'] != $event_item['multiday']['start_date']))
										? r34ics_date('c', ($event->dtend_array[2] ?? $event->dtstart_array[2]), $event_end_tz, '+1 day')
										: wp_date('c', ($event->dtend_array[2] ?? $event->dtstart_array[2]), $event_end_tz)
								),
								'groupId' => (string)$feed_key,
								'id' => (string)$event->uid,
								'overlap' => true,
								'start' => wp_date('c', $event->dtstart_array[2], $event_start_tz),
								'url' => empty($args['nolink']) ? ($event->url ?: ($event->uri ?: '')) : '',
								'extendedProps' => array(),
							);
							
							// Scrape a URL from the description
							// Must come before we generate event description HTML!
							// Function was added in ICS Calendar 10.8.6; this check prevents fatal errors on sites that haven't updated yet
							if (empty($args['nolink']) && function_exists('r34ics_scrape_url_from_string')) {
								if (!empty($args['linktitles']) && empty($fc_event['url']) && !empty($event->description)) {
									$fc_event['url'] = r34ics_scrape_url_from_string($event->description, false, true);
								}
							}
							
							// Add event description HTML
							$has_desc = true; // Full view *always* needs the description overlay
							$fc_event['extendedProps']['event_description_html'] = $this->event_description_html($args, $event_item, $classes, $has_desc);

							/*
								We want to apply filters to the event title to allow regular expressions
								and custom filtering, but not generate the full ICS "label" HTML, so we
								just use the filter instead of the R34ICS::event_label_html() method
							*/
							$title = (!empty($args['maskinfo']) ? $args['maskinfo'] : ($event->summary ?: ''));
							$fc_event['title'] = apply_filters('r34ics_event_label_html_filter', $title, $args, $fc_event, $classes);

							// Set event CSS classes
							$fc_event['classNames'] = apply_filters('r34ics_event_css_classes', array('event'), $fc_event, '', $args);

							// Set event color
							if (!empty($ics_data['colors'][$feed_key]['base'])) {
								$fc_event['color'][] = $ics_data['colors'][$feed_key]['base'];
							}
							
							// Cross-compatibility with other views that use this method (e.g. up-next)
							if ($args['view'] != 'full') {
								$fc_event['feed_key'] = $fc_event['groupId'];
								$fc_event['label'] = $fc_event['title'];
								$fc_event['start_timestamp'] = wp_date('U', $event->dtstart_array[2], $event_start_tz);
								$fc_event['end_timestamp'] = wp_date('U', $event->dtend_array[2], $event_end_tz);
								// Add start and end times for description
								$fc_event['start_time'] = $event_item['start'];
								$fc_event['end_time'] = $event_item['end'];
								foreach ((array)$event_item as $key => $value) {
									if (empty($fc_event[$key])) { $fc_event[$key] = $value; }
								}
							}
							
							// Add this event to main events array
							$ics_data['events'][] = apply_filters('r34ics_display_calendar_event_item', $fc_event, $event, $args);
						
						}
					
					}
					
					// Debugging information (must occur outside conditional in case it evaluates to false)
					if ($this->debug >= 2) {
						/* This may output a very large amount of data */
						$this->debug_messages[$url]['ICS Parser Data'] = !isset($ics_events) ? 'NO DATA' : $ics_events;
					}
					$this->debug_messages[$url]['Events parsed'] = !isset($ics_events) ? 0 : count($ics_events);
		
				}

				// Write ICS data to transient
				// Allow $reload as an integer > 1 to represent the cache expiration in seconds; otherwise use the regular option
				$transient_expiration = (intval($args['reload']) > 1) ? intval($args['reload']) : get_option('r34ics_transients_expiration');
				set_transient($transient_name, $ics_data, $transient_expiration);

			}
	
			// Debugging
			if (!empty($this->debug)) {
				if (empty($ics_data['events'])) {
					if (!empty($loaded_from_transient)) {
						$this->debug_messages['Errors'][] = 'Unavailable -- loaded from transient; set reload="true" in shortcode to force reload.';
					}
					else {
						$this->debug_messages['Errors'][] = 'Unable to parse ICS data.';
					}
				}
				$current_memory_usage = memory_get_usage();
				if (!isset($this->debug_messages['Peak memory usage']) || $current_memory_usage > $this->debug_messages['Peak memory usage']) {
					$this->debug_messages['Peak memory usage'] = memory_get_usage();
					$this->debug_messages['Peak memory usage'] = size_format($this->debug_messages['Peak memory usage'], 2);
				}
				if ($this->debug >= 2) {
					$this->debug_messages['Plugin Data'] = $ics_data;
				}
				r34ics__debug($this->debug_messages);
			}
					
			// Remove events with redundant UIDs on the same date
			if (!empty($args['fixredundantuids'])) {
				add_filter('r34ics_display_calendar_filter_ics_data', 'r34icspro_scrub_duplicate_uids_full_view', 10, 2);
			}
			
			// Allow external customization of ICS data
			$ics_data = apply_filters('r34ics_display_calendar_filter_ics_data', $ics_data, $args);
		
			// Convert data array to JSON (for FullCalendar.io)
			$ics_data['events_json'] = json_encode($ics_data['events']);

			// Allow external customization of ICS data
			$ics_data = apply_filters('r34ics_get_ics_json_events', $ics_data, $args);
		
			return $ics_data;
		}
		
		
		protected function _get_version() {
			if (!function_exists('get_plugin_data')) {
				require_once(ABSPATH . 'wp-admin/includes/plugin.php');
			}
			$plugin_data = get_plugin_data(dirname(dirname(__FILE__)) . '/ics-calendar-pro.php', false, false);
			return $plugin_data['Version'];
		}
	
	
	}

}