<?php
use EM_Event_Locations\Event_Locations;
use EM\Archetypes;
//TODO EM_Events is currently static, better we make this non-static so we can loop sets of events, and standardize with other objects.
/**
 * Use this class to query and manipulate sets of events. If dealing with more than one event, you probably want to use this class in some way.
 *
 */
class EM_Events extends EM_Object {
	
	/**
	 * Like WPDB->num_rows it holds the number of results found on the last query.
	 * @var int
	 */
	public static $num_rows;
	
	/**
	 * If $args['pagination'] is true or $args['offset'] or $args['page'] is greater than one, and a limit is imposed when using a get() query, 
	 * this will contain the total records found without a limit for the last query.
	 * If no limit was used or pagination was not enabled, this will be the same as self::$num_rows
	 * @var int
	 */
	public static $num_rows_found;
	
	protected static $context = 'event';
	
	/**
	 * Returns an array of EM_Events that match the given specs in the argument, or returns a list of future evetnts in future 
	 * (see EM_Events::get_default_search() ) for explanation of possible search array values. You can also supply a numeric array
	 * containing the ids of the events you'd like to obtain 
	 * 
	 * @param array $args
	 * @return EM_Event[]
	 */
	public static function get( $args = array(), $count=false ) {
		global $wpdb;	 
		$events_table = EM_EVENTS_TABLE;
		$events_table_from = $events_table;
		$locations_table = EM_LOCATIONS_TABLE;
		
		//Quick version, we can accept an array of IDs, which is easy to retrieve
		if( self::array_is_numeric($args) ){ //Array of numbers, assume they are event IDs to retreive
			//We can just get all the events here and return them
			$events = array();
			foreach($args as $event_id){
				$events[$event_id] = em_get_event($event_id);
			}
			return apply_filters('em_events_get', $events, $args);
		}
		
		//We assume it's either an empty array or array of search arguments to merge with defaults			
		$args = self::get_default_search($args);
		$limit = ( $args['limit'] && is_numeric($args['limit'])) ? "LIMIT {$args['limit']}" : '';
		$offset = ( $limit != "" && is_numeric($args['offset']) ) ? "OFFSET {$args['offset']}" : '';
		
		//Get fields that we can use in ordering and grouping, which can be event and location (excluding ambiguous) fields
		$accepted_fields = static::get_sql_accepted_fields();
		
		//Start SQL statement
		
		//Create the SQL statement selectors
		$calc_found_rows = $limit && ( $args['pagination'] || $args['offset'] > 0 || $args['page'] > 0 );
		if( $count ){
			$selectors = 'COUNT(DISTINCT '.$events_table . '.event_id)';
			$limit = 'LIMIT 1';
			$offset = 'OFFSET 0';
		}else{
			if( $args['array'] ){
				//get all fields from table, add events table prefix to avoid ambiguous fields from location
				$selectors = $events_table . '.*';
			}elseif( EM_MS_GLOBAL ){
				$selectors = $events_table.'.event_id, '. $events_table.'.post_id , ' . $events_table.'.blog_id';
			}else{
				$selectors = $events_table.'.event_id, '. $events_table.'.post_id';
			}
			if ( !empty($args['timeslots']) ) {
				if ( $args['array'] ) {
					// add as first selectors so that null event_id values are overwritten by the wp_em_events table fields
					$selectors = 'ets.*, ' . $selectors;
				} else {
					$selectors .= ', ets.timeslot_id';
				}
			}
			$selectors = 'DISTINCT ' . $selectors; //duplicate avoidance
		}
		
		//check if we need to join a location table for this search, which is necessary if any location-specific are supplied, or if certain arguments such as orderby contain location fields
		$EM_Location = new EM_Location();
		$location_fields = array_intersect($accepted_fields, array_keys($EM_Location->fields));
		if( !empty($args['groupby']) || (defined('EM_DISABLE_OPTIONAL_JOINS') && EM_DISABLE_OPTIONAL_JOINS) ){
			$location_specific_args = array('town', 'state', 'country', 'region', 'near', 'geo', 'search');
			$join_locations = false;
			foreach( $location_specific_args as $arg ) if( !empty($args[$arg]) ) $join_locations = true;
			//if set to false the following would provide a false negative in the line above
			if( $args['location_status'] !== false ){ $join_locations = true; }
			//check ordering and grouping arguments for precense of location fields requiring a join
			if( !$join_locations ){
				foreach( array('groupby', 'orderby', 'groupby_orderby') as $arg ){
					if( !is_array($args[$arg]) ) continue; //ignore this argument if set to false
					//we assume all these arguments are now array thanks to self::get_search_defaults() cleaning it up
					foreach( $args[$arg] as $field_name ){
						if( in_array($field_name, $location_fields) ){
							$join_locations = true;
							break; //we join, no need to keep searching
						}
					}
				}
			}
		}else{ $join_locations = true; }//end temporary if( !empty($args['groupby']).... wrapper
		//plugins can override this optional joining behaviour here in case they add custom WHERE conditions or something like that
		$join_locations = apply_filters('em_events_get_join_locations_table', $join_locations, $args, $count);
		//depending on whether to join we do certain things like add a join SQL, change specific values like status search
		$location_optional_join = $join_locations ? "LEFT JOIN $locations_table ON {$locations_table}.location_id={$events_table}.location_id" : '';

		// split up results by timeslot_ids if requested
		if ( !empty( $args['timeslots'] ) ) {
			$events_table_from .= ' LEFT JOIN ' . EM_EVENT_TIMESLOTS_TABLE  . ' ets ON '. EM_EVENTS_TABLE .'.event_id = ets.event_id';
		}
		
		//Build ORDER BY and WHERE SQL statements here, after we've done all the pre-processing necessary
		$conditions = self::build_sql_conditions($args);
		$where = ( count($conditions) > 0 ) ? " WHERE " . implode ( " AND ", $conditions ):'';
		$orderby = self::build_sql_orderby($args, $accepted_fields, em_get_option('dbem_events_default_order'));
		$orderby_sql = ( count($orderby) > 0 ) ? 'ORDER BY '. implode(', ', $orderby) : '';
		
		//Build GROUP BY SQL statement, which will be very different if we group things due to how we need to filter out by event date
		if( !empty($args['groupby']) ){
			//get groupby field(s)
			$groupby_fields = self::build_sql_groupby($args, $accepted_fields);
			if( !empty($groupby_fields[0]) ){
				//we can safely assume we've been passed at least one array item with index of 0 containing a valid field due to build_sql_groupby()
				$groupby_field = $groupby_fields[0]; //we only support one field for events
				$groupby_orderby = self::build_sql_groupby_orderby($args, $accepted_fields);
				$groupby_orderby_sql = !empty($groupby_orderby) ? ', '. implode(', ', $groupby_orderby) : '';
				//get minimum required selectors within the inner query to shorten query length as much as possible
				$inner_selectors = $events_table . '.*';
				if( $location_optional_join ){
					//we're selecting all fields from events table so add only location fields required in the outer ORDER BY statement
					foreach( $args['orderby'] as $orderby_field ){
						if( in_array($orderby_field, $location_fields) ){
							$inner_selectors .= ', '. $locations_table .'.'. $orderby_field;
						}
					}
				}
				
				//THE Query - Grouped
				if( in_array($groupby_field, $location_fields) || count(array_intersect($location_fields, $args['groupby_orderby'])) || defined('EM_FORCE_GROUPED_DATASET_SQL') ){
					//if we're grouping by any fields in the locations table, we run a different (slightly slower) query to provide reliable results
					if( in_array($groupby_field, $location_fields) && !in_array($groupby_field, $args['orderby']) ){
						//we may not have included the grouped field if it's not in the outer ORDER BY clause, so we add it for this specific query
						$inner_selectors .= ', '. $locations_table .'.'. $groupby_field;
					}
					$sql = "
SELECT $selectors
FROM (
	SELECT *,
		@cur := IF($groupby_field = @id, @cur+1, 1) AS RowNumber,
		@id := $groupby_field AS IdCache
	FROM (
		SELECT {$inner_selectors} FROM {$events_table_from}
		$location_optional_join
		$where
		ORDER BY $groupby_field $groupby_orderby_sql
	) {$events_table}
	INNER JOIN (
		SELECT @id:=0, @cur:=0
	) AS lookup
) {$events_table}
WHERE RowNumber = 1
$orderby_sql";
					$sql_count = str_replace( $selectors, ' COUNT(*) ', $sql );
					$sql .= "\n". "$limit $offset";
				}else{
					//we'll keep this query simply because it's a little faster and still seems reliable when not grouping or group-sorting any fields in the locations table
					$sql = "
SELECT $selectors
FROM (
	SELECT {$inner_selectors},
		@cur := IF($groupby_field = @id, @cur+1, 1) AS RowNumber,
		@id := $groupby_field AS IdCache
	FROM {$events_table_from}
	INNER JOIN (
		SELECT @id:=0, @cur:=0
	) AS lookup
	$location_optional_join
	$where
	ORDER BY {$groupby_field} $groupby_orderby_sql
) {$events_table}
WHERE RowNumber = 1
$orderby_sql";
					$sql_count = str_replace( $selectors, ' COUNT(*) ', $sql );
					$sql .= "\n". "$limit $offset";
				}
			}
		}
		
		//Build THE Query SQL statement if not already built for a grouped query
		if( empty($sql) ){
			//THE Query
			$sql = "
SELECT $selectors FROM $events_table_from
$location_optional_join
$where
$orderby_sql";
			$sql_count = str_replace( $selectors, ' COUNT(*) ', $sql );
			$sql .= "\n". "$limit $offset";
		}
	
		//THE Query filter
		$sql = apply_filters('em_events_get_sql', $sql, $args);
		if ( has_filter('em_events_get_sql') && !has_filter('em_events_get_sql_count') ) {
			// backwards compatibility here, try and replace the SQL selector with COUNT(*) and remove the limit/offset via a preg_replace
			$sql_count = preg_replace( '/SELECT[\s\S]*?FROM/i', 'SELECT COUNT(*) FROM', $sql, 1 );
			$sql_count = preg_replace( '/\s+LIMIT\s+\d+(?:\s*,\s*\d+)?/i', '', $sql_count );
			$sql_count = preg_replace( '/\s+OFFSET\s+\d+/i', '', $sql_count );
		} else {
			$sql_count = apply_filters('em_events_get_sql_count', $sql_count ?? $sql, $args);
		}
		//if( em_wp_is_super_admin() && WP_DEBUG_DISPLAY ){ echo "<pre>"; print_r($sql); echo '</pre>'; }
				
		//If we're only counting results, return the number of results and go no further
		if( $count ){
			self::$num_rows_found = self::$num_rows = $wpdb->get_var($sql_count);
			return apply_filters('em_events_get_count', self::$num_rows, $args);		
		}
		
		//get the result and count results
		$results = $wpdb->get_results( $sql, ARRAY_A);
		self::$num_rows = $wpdb->num_rows;
		if( $calc_found_rows ){
			self::$num_rows_found = $wpdb->get_var($sql_count);
		}else{
			self::$num_rows_found = self::$num_rows;
		}

		//If we want results directly in an array, why not have a shortcut here?
		if( $args['array'] == true ){
			if ( !empty($args['timeslots']) ) {
				// convert all the results to timeslot dates
				foreach ( $results as $key => $result ) {
					// if this is not a timeslot event, i.e. no timeslot data, skip it
					if ( !empty( $result['timeslot_id'] ) ) {
						$results[$key]['event_start'] = $result['timeslot_start'];
						$results[$key]['event_end'] = $result['timeslot_end'];
						// convert event_start_date, event_start_time and end couterparts to timezone-correct times
						$start = new EM_DateTime( $result['timeslot_start'], 'UTC' );
						$start->setTimezone( $result['event_timezone'] );
						$results[$key]['event_start_date'] = $start->getDate();
						$results[$key]['event_start_time'] = $start->getTime();
						$end = new EM_DateTime( $result['timeslot_end'], 'UTC' );
						$end->setTimezone( $result['event_timezone'] );
						$results[$key]['event_end_date'] = $end->getDate();
						$results[$key]['event_end_time'] = $end->getTime();
					}
				}
			}
			return apply_filters('em_events_get_array',$results, $args);
		}
		
		//Make returned results EM_Event objects
		$results = (is_array($results)) ? $results : [];
		$events = $timeslot_ids = [];
		
		if( EM_MS_GLOBAL ){
			foreach ( $results as $event ){
				if ( !empty($event['post_id']) ) {
					$EM_Event = em_get_event( $event['post_id'], $event['blog_id'] );
				} else {
					$EM_Event = em_get_event( $event['event_id'] );
				}
				$event_id = $EM_Event->event_id;
				// set timeslot id and append to event_id for later reference
				if ( !empty($event['timeslot_id']) ) {
					$EM_Event->convert_to_timeslot( $event['timeslot_id'], false );
				}
				$events[$event_id] = $EM_Event;
			}
		}else{
			foreach ( $results as $event ){
				if ( !empty($event['post_id']) ) {
					$EM_Event = em_get_event( $event['post_id'], 'post_id' );
					// convert to timeslot if needed
					if ( !empty($event['timeslot_id']) ) {
						$EM_Event->convert_to_timeslot( $event['timeslot_id'], false );
					}
				} else {
					// set timeslot id and append to event_id for later reference
					$event_id = !empty($event['timeslot_id']) ? $event['event_id'] . ':' . $event['timeslot_id'] : $event['event_id'];
					$EM_Event = em_get_event( $event_id );
				}
				$events[$EM_Event->event_id] = $EM_Event;
			}
		}

		// get all the timeslot data in one SQL query (for efficiency) and override it here in one go rather than individual set_timeslot_id() calls
		if ( !empty($args['timeslots']) && $timeslot_ids ) {
			$sql = "SELECT * FROM " . EM_EVENT_TIMESLOTS_TABLE . " WHERE timeslot_id IN (" . implode(',', $timeslot_ids) . ")";
			$timeslots = $wpdb->get_results($sql, ARRAY_A);
			foreach ( $timeslots as $timeslot ) {
				if ( !empty( $events[ $timeslot['event_id'] . ':' . $timeslot['timeslot_id'] ] ) ) {
					$EM_Event = $events[ $timeslot['event_id'] . ':' . $timeslot['timeslot_id'] ];
					$events[ $timeslot['event_id'] . ':' . $timeslot['timeslot_id'] ] = $EM_Event->convert_to_timeslot( $timeslot['timeslot_id'], false );
				}
			}
		}

		return apply_filters('em_events_get', $events, $args);
	}
	
	/**
	 * Returns the number of events on a given date
	 * @param $date
	 * @return int
	 */
	public static function count_date($date){
		global $wpdb;
		$table_name = EM_EVENTS_TABLE;
		$sql = "SELECT COUNT(*) FROM  $table_name WHERE (event_start_date  like '$date') OR (event_start_date <= '$date' AND event_end_date >= '$date');";
		return apply_filters('em_events_count_date', $wpdb->get_var($sql));
	}
	
	public static function count( $args = array() ){
		return apply_filters('em_events_count', self::get($args, true), $args);
	}
	
	/**
	 * Will delete given an array of event_ids or EM_Event objects
	 * @param EM_Event[]|int[] $id_array
	 */
	public static function delete( $array ){
		//Detect array type and generate SQL for event IDs
		$results = array();
		$event_ids = array();
		if ( is_array( $array ) ) {
			if( !empty($array) && !( current($array) instanceof EM_Event ) ) {
				$events = self::get($array);
			}
			foreach ($events as $EM_Event){
				if ( !empty($EM_Event->event_id) ) {
					$event_ids[] = $EM_Event->event_id;
					$results[] = $EM_Event->delete();
				}
			}
		}
		//TODO add better error feedback on events delete fails
		return apply_filters('em_events_delete',  in_array(false, $results), $event_ids);
	}
	
	
	/**
	 * Output a set of matched of events. You can pass on an array of EM_Events as well, in this event you can pass args in second param.
	 * Note that you can pass a 'pagination' boolean attribute to enable pagination, default is enabled (true). 
	 * @param array $args
	 * @param array $secondary_args
	 * @return string
	 */
	public static function output( $args ){
		global $EM_Event;
		$EM_Event_old = $EM_Event; //When looping, we can replace EM_Event global with the current event in the loop
		//get page number if passed on by request (still needs pagination enabled to have effect)
		$page_queryvar = !empty($args['page_queryvar']) ? $args['page_queryvar'] : 'pno';
		if( !empty($args['pagination']) && !array_key_exists('page',$args) && !empty($_REQUEST[$page_queryvar]) && is_numeric($_REQUEST[$page_queryvar]) ){
			$args['page'] = $_REQUEST[$page_queryvar];
		}
		//Can be either an array for the get search or an array of EM_Event objects
		if( is_object(current($args)) && get_class((current($args))) == 'EM_Event' ){
			$func_args = func_get_args();
			$events = $func_args[0]; /* @var EM_Event[] $events */
			$args = (!empty($func_args[1]) && is_array($func_args[1])) ? $func_args[1] : array();
			$args = apply_filters('em_events_output_args', self::get_default_search($args), $events);
			$limit = ( !empty($args['limit']) && is_numeric($args['limit']) ) ? $args['limit']:false;
			$events_count = count($events);
		}else{
			//Firstly, let's check for a limit/offset here, because if there is we need to remove it and manually do this
			$args = apply_filters('em_events_output_args', self::get_default_search($args));
			$limit = ( !empty($args['limit']) && is_numeric($args['limit']) ) ? $args['limit']:false;
			$events = self::get( $args );
			$events_count = self::$num_rows_found;
		}
		//What format shall we output this to, or use default
		$format = ( empty($args['format']) ) ? em_get_option( 'dbem_event_list_item_format' ) : $args['format'] ;
		
		$output = "";
		
		if ( $events_count > 0 ) {
			$events = apply_filters('em_events_output_events', $events, $events_count, $args);
			foreach ( $events as $EM_Event ) {
				$output .= $EM_Event->output($format);
			} 
			//Add headers and footers to output
			if( $format == em_get_option( 'dbem_event_list_item_format' ) ){
			    //we're using the default format, so if a custom format header or footer is supplied, we can override it, if not use the default
			    $format_header = empty($args['format_header']) ? em_get_option('dbem_event_list_item_format_header') : $args['format_header'];
			    $format_footer = empty($args['format_footer']) ? em_get_option('dbem_event_list_item_format_footer') : $args['format_footer'];
			}else{
			    //we're using a custom format, so if a header or footer isn't specifically supplied we assume it's blank
			    $format_header = !empty($args['format_header']) ? $args['format_header'] : '' ;
			    $format_footer = !empty($args['format_footer']) ? $args['format_footer'] : '' ;
			}
			$output = $format_header .  $output . $format_footer;
			//Pagination (if needed/requested)
			if( !empty($args['pagination']) && !empty($limit) && $events_count > $limit ){
				$output .= self::get_pagination_links($args, $events_count);
			}
		}elseif( $args['no_results_msg'] !== false ){
			$output = !empty($args['no_results_msg']) ? esc_html($args['no_results_msg']) : em_get_option('dbem_no_events_message');
		}
		
		//TODO check if reference is ok when restoring object, due to changes in php5 v 4
		$EM_Event = $EM_Event_old;
		$output = apply_filters('em_events_output', $output, $events, $args, $events_count);
		return $output;		
	}
	
	/**
	 * Generate a grouped list of events by year, month, week or day.
	 * 
	 * There is a nuance with this function, long_events won't work unless you add a limit of 0. The reason is because this won't work with pagination, due to the fact
	 * that you need to alter the event total count to reflect each time an event is displayed in a time range. e.g. if an event lasts 2 days and it's daily grouping,
	 * then that event would count as 2 events for pagination purposes. For that you need to count every single event and calculate date range etc. which is too resource
	 * heavy and not scalabale, therefore we've added this limitation.
	 * 
	 * @since 5.4.4.2
	 * @param array $args
	 * @return string
	 */
	public static function output_grouped( $args = array() ){
		//Reset some args to include pagination for if pagination is requested.
		$args['limit'] = isset($args['limit']) ? $args['limit'] : em_get_option('dbem_events_default_limit');
		$args['page'] = (!empty($args['page']) && is_numeric($args['page']) )? $args['page'] : 1;
		$args['page'] = (!empty($args['pagination']) && !empty($_REQUEST['pno']) && is_numeric($_REQUEST['pno']) )? $_REQUEST['pno'] : $args['page'];
		$args['offset'] = ($args['page']-1) * $args['limit'];
		$args['orderby'] = 'event_start_date,event_start_time,event_name'; // must override this to display events in right cronology.
		$long_events = !empty($args['long_events']);

		$args['mode'] = !empty($args['mode']) ? $args['mode'] : em_get_option('dbem_event_list_groupby');
		$args['header_format'] = !empty($args['header_format']) ? $args['header_format'] :  em_get_option('dbem_event_list_groupby_header_format', '<h2>#s</h2>');
		$args['date_format'] = !empty($args['date_format']) ? $args['date_format'] :  em_get_option('dbem_event_list_groupby_format','');
		$args = apply_filters('em_events_output_grouped_args', self::get_default_search($args));
		//Reset some vars for counting events and displaying set arrays of events
		$atts = (array) $args;
		$atts['pagination'] = $atts['limit'] = $atts['page'] = $atts['offset'] = false;
		//decide what form of dates to show
		$EM_Events = self::get( $args );
		$events_count = self::$num_rows_found;
		ob_start();
		if( $events_count > 0 ){
			switch ( $args['mode'] ){
				case 'yearly':
					//go through the events and put them into a monthly array
					$format = (!empty($args['date_format'])) ? $args['date_format']:'Y';
					$events_dates = array();
					foreach($EM_Events as $EM_Event){ /* @var $EM_Event EM_Event */
						$year = $EM_Event->start()->format('Y');
						$events_dates[$year][] = $EM_Event;
						//if long events requested, add event to other dates too
						if( empty($args['limit']) && $long_events && $EM_Event->end()->getDate() != $EM_Event->start()->getDate() ) {
							$next_year = $year + 1;
							$year_end = $EM_Event->end()->format('Y');
							while( $next_year <= $year_end ){
								$events_dates[$next_year][] = $EM_Event;
								$next_year = $next_year + 1;
							}
						}
					}
					$events_dates = apply_filters('em_events_output_grouped_events_dates', $events_dates, $args);
					foreach ($events_dates as $year => $events){
						$EM_DateTime = new EM_DateTime($year.'-01-01');
						echo str_replace('#s', $EM_DateTime->i18n($format), $args['header_format']);
						echo self::output($events, $atts);
					}
					break;
				case 'monthly':
					//go through the events and put them into a monthly array
					$format = (!empty($args['date_format'])) ? $args['date_format']:'M Y';
					$events_dates = array();
					foreach($EM_Events as $EM_Event){
						$events_dates[$EM_Event->start()->format('Y-m-01')][] = $EM_Event;
						//if long events requested, add event to other dates too
						if( empty($args['limit']) && $long_events && $EM_Event->end()->getDate() != $EM_Event->start()->getDate() ) {
							///$EM_DateTime is synoymous with the next month here
							$EM_DateTime = $EM_Event->start()->copy()->add('P1M');
							while( $EM_DateTime <= $EM_Event->end() ){
								$events_dates[$EM_DateTime->format('Y-m-01')][] = $EM_Event;
								$EM_DateTime = $EM_DateTime->add('P1M');
							}
						}
					}
					$events_dates = apply_filters('em_events_output_grouped_events_dates', $events_dates, $args);
					foreach ($events_dates as $month => $events){
						$EM_DateTime = new EM_DateTime($month);
						echo str_replace('#s', $EM_DateTime->i18n($format), $args['header_format']);
						echo self::output($events, $atts);
					}
					break;
				case 'weekly':
					$format = (!empty($args['date_format'])) ? $args['date_format']:em_get_option('date_format');
					$events_dates = array();
					foreach($EM_Events as $EM_Event){
						//obtain start of the week as per WordPress general settings
			   			$start_of_week = em_get_option('start_of_week');
						$day_of_week = $EM_Event->start()->format('w');
						$offset = $day_of_week - $start_of_week;
						if($offset<0){ $offset += 7; }
						$EM_DateTime = $EM_Event->start()->copy()->sub('P'.$offset.'D');
						//save event to date representing start of week for this WP install based on general settings
						$events_dates[$EM_DateTime->getDate()][] = $EM_Event;
						//if long events requested, add event to other dates too
						if( empty($args['limit']) && $long_events && $EM_Event->end()->getDate() != $EM_Event->start()->getDate() ) {
							do{
								$EM_DateTime->add('P1W');
								$events_dates[$EM_DateTime->getDate()][] = $EM_Event;
							}while( $EM_DateTime <= $EM_Event->end() );
						}
					}
					$events_dates = apply_filters('em_events_output_grouped_events_dates', $events_dates, $args);
					foreach ($events_dates as $date => $events){
						$dates_formatted = $EM_DateTime->modify($date)->i18n($format). em_get_option('dbem_dates_separator') . $EM_DateTime->add('P6D')->i18n($format);
						echo str_replace('#s', $dates_formatted, $args['header_format']);
						echo self::output($events, $atts);
					}
					break;
				default: //daily
					//go through the events and put them into a daily array
					$format = (!empty($args['date_format'])) ? $args['date_format']:em_get_option('date_format');
					$events_dates = array();
					foreach($EM_Events as $EM_Event){
						$EM_DateTime = $EM_Event->start()->copy()->setTime(0,0,0); /* @var EM_DateTime $EM_DateTime */
						$events_dates[$EM_DateTime->getDate()][] = $EM_Event;
						//if long events requested, add event to other dates too
						if( empty($args['limit']) && $long_events && $EM_Event->end()->getDate() != $EM_Event->start()->getDate() ) {
							do{
								$EM_DateTime->add('P1D');
								//store indexes as Y-m-d format so we become timezone independent
								$events_dates[$EM_DateTime->getDate()][] = $EM_Event;
							}while( $EM_DateTime <= $EM_Event->end() );
						}
					}
					$events_dates = apply_filters('em_events_output_grouped_events_dates', $events_dates, $args);
					foreach ($events_dates as $date => $events){
						echo str_replace('#s', $EM_DateTime->modify($date)->i18n($format), $args['header_format']);
						echo self::output($events, $atts);
					}
					break;
			}
			//Show the pagination links (unless there's less than $limit events)
			if( !empty($args['pagination']) && !empty($args['limit']) && $events_count > $args['limit'] ){
				$default_args = self::get_default_search();
				$default_args['limit'] = em_get_option('dbem_events_default_limit');
				$default_args['mode'] = em_get_option('dbem_event_list_groupby');
				$default_args['header_format'] = em_get_option('dbem_event_list_groupby_header_format', '<h2>#s</h2>');
				$default_args['date_format'] = em_get_option('dbem_event_list_groupby_format','');
				echo self::get_pagination_links($args, $events_count, 'search_events', $default_args);
			}
		}elseif( $args['no_results_msg'] !== false ){
			echo !empty($args['no_results_msg']) ? esc_html($args['no_results_msg']) : em_get_option('dbem_no_events_message');
		}
		return apply_filters('em_events_output_grouped', ob_get_clean(), $EM_Events, $args, $events_count);
	}
	
	public static function get_pagination_links($args, $count, $search_action = 'search_events', $default_args = array()){
		//get default args if we're in a search, supply to parent since we can't depend on late static binding until WP requires PHP 5.3 or later
		if( empty($default_args) && (!empty($args['ajax']) || !empty($_REQUEST['action']) && $_REQUEST['action'] == $search_action) ){
			$default_args = self::get_default_search();
			$default_args['limit'] = em_get_option('dbem_events_default_limit');
		}
		return parent::get_pagination_links($args, $count, $search_action, $default_args);
	}

	/**
	 * Adds JavaScript variables to the page load, useful for dynamically loading localized text specifically for the event editor when loaded.
	 * @return void
	 */
	public static function add_editor_js_vars() {
		$js_vars = [
			'deleteTicketWarning' => sprintf( esc_html__('Are you sure you want to delete this ticket? All bookings related to this ticket will be deleted once you save this %s.','events-manager'), esc_html__('Event', 'events-manager') ),
			'deleteTicketParentWarning' => esc_html__('Are you sure you want to delete this ticket?', 'events-manager') . "\n\n" . esc_html__('Tickets within recurrences without any bookings will be deleted.', 'events-manager')  . "\n\n" . esc_html__('Tickets within recurrences that already have bookings WILL NOT be deleted, the ticket will be deactivated instead to prevent further bookings.','events-manager'),
		];
		EM\Scripts_and_Styles::add_js_var('eventEditor', $js_vars);
		// add recurrence set stuff too
		EM\Recurrences\Recurrence_Sets::add_js_vars();
	}
	
	/* (non-PHPdoc)
	 * DEPRECATED - this class should just contain static classes,
	 * @see EM_Object::can_manage()
	 */
	function can_manage($event_ids = false , $admin_capability = false, $user_to_check = false ){
		global $wpdb;
		if( current_user_can('edit_others_events') ){
			return apply_filters('em_events_can_manage', true, $event_ids);
		}
		if( EM_Object::array_is_numeric($event_ids) ){
			$condition = implode(" OR event_id=", $event_ids);
			//we try to find any of these events that don't belong to this user
			$results = $wpdb->get_var("SELECT COUNT(*) FROM ". EM_EVENTS_TABLE ." WHERE event_owner != '". get_current_user_id() ."' event_id=$condition;");
			return apply_filters('em_events_can_manage', ($results == 0), $event_ids);
		}
		return apply_filters('em_events_can_manage', false, $event_ids);
	}
	
	public static function get_post_search($args = array(), $filter = false, $request = array(), $accepted_args = array()){
		//supply $accepted_args to parent argument since we can't depend on late static binding until WP requires PHP 5.3 or later
		$accepted_args = !empty($accepted_args) ? $accepted_args : array_keys(self::get_default_search());
		return apply_filters('em_events_get_post_search', parent::get_post_search($args, $filter, $request, $accepted_args));
	}

	/* Overrides EM_Object method to apply a filter to result
	 * @see wp-content/plugins/events-manager/classes/EM_Object#build_sql_conditions()
	 */
	public static function build_sql_conditions( $args = array() ){
		global $wpdb;
		//continue with conditions
		$conditions = parent::build_sql_conditions($args);
		//specific location query conditions if locations are enabled
		if( em_get_option('dbem_locations_enabled') ){
			//events with or without locations
			if( !empty($args['has_location']) ){
				$conditions['has_location'] = '('.EM_EVENTS_TABLE.'.location_id IS NOT NULL AND '.EM_EVENTS_TABLE.'.location_id != 0)';
			}elseif( !empty($args['no_location']) ){
				$conditions['no_location'] = '('.EM_EVENTS_TABLE.'.location_id IS NULL OR '.EM_EVENTS_TABLE.'.location_id = 0)';			
			}elseif( !empty($conditions['location_status']) ){
				$location_specific_args = array('town', 'state', 'country', 'region', 'near', 'geo', 'search');
				foreach( $location_specific_args as $location_arg ){
					if( !empty($args[$location_arg]) ) $skip_location_null_condition = true;
				}
				if( empty($skip_location_null_condition) ){
					$conditions['location_status'] = '('.$conditions['location_status'].' OR '.EM_LOCATIONS_TABLE.'.location_id IS NULL)';
				}
			}
		}
		// recurrence set
		if ( !empty($args['recurrence_set']) ){
			if ( is_numeric( $args['recurrence_set'] ) ){
				$conditions['recurrence_set'] = "(`recurrence_set_id`=" . absint($args['recurrence_set']).")";
			} elseif ( static::array_is_numeric( $args['recurrence_set'] ) ){
				$conditions['recurrence_set'] = "(`recurrence_set_id` IN (" . implode(',', $args['recurrence_set']).") )";
			}
		}
		//search conditions
		if( !empty($args['search']) ){
			if( em_get_option('dbem_locations_enabled') ){
				$like_search = array('event_name',EM_EVENTS_TABLE.'.post_content','location_name','location_address','location_town','location_postcode','location_state','location_country','location_region');
			}else{
				$like_search = array('event_name',EM_EVENTS_TABLE.'.post_content');
			}
			$like_search_string = '%'.$wpdb->esc_like($args['search']).'%';
			$like_search_strings = array();
			foreach( $like_search as $v ) $like_search_strings[] = $like_search_string;
			$like_search_sql = "(".implode(" LIKE %s OR ", $like_search). "  LIKE %s)";
			$conditions['search'] = $wpdb->prepare($like_search_sql, $like_search_strings);
		}
		//private events
		if( empty($args['private']) ){
			$conditions['private'] = "(`event_private`=0)";			
		}elseif( !empty($args['private_only']) ){
			$conditions['private_only'] = "(`event_private`=1)";
		}
		if( EM_MS_GLOBAL && !empty($args['blog']) ){
		    if( is_numeric($args['blog']) ){
				if( is_main_site($args['blog']) ){
					$conditions['blog'] = "(".EM_EVENTS_TABLE.".blog_id={$args['blog']} OR ".EM_EVENTS_TABLE.".blog_id IS NULL)";
				}else{
					$conditions['blog'] = "(".EM_EVENTS_TABLE.".blog_id={$args['blog']})";
				}
		    }else{
		        if( !is_array($args['blog']) && preg_match('/^([\-0-9],?)+$/', $args['blog']) ){
		            $conditions['blog'] = "(".EM_EVENTS_TABLE.".blog_id IN ({$args['blog']}) )";
			    }elseif( is_array($args['blog']) && self::array_is_numeric($args['blog']) ){
			        $conditions['blog'] = "(".EM_EVENTS_TABLE.".blog_id IN (".implode(',',$args['blog']).") )";
			    }
		    }
		}
		//post search
		if( !empty($args['post_id'])){
			if( is_array($args['post_id']) ){
				$conditions['post_id'] = "(".EM_EVENTS_TABLE.".post_id IN (".implode(',',$args['post_id'])."))";
			} elseif ( $args['post_id'] === true || $args['post_id'] === 'true' ) {
				// if set to true specifically (or 'true' in a shortcode, we search for all events that DO HAVE a post ID (recurrences)
				$conditions['post_id'] = "(".EM_EVENTS_TABLE.".post_id > 0 )";
			} else {
				$conditions['post_id'] = "(".EM_EVENTS_TABLE.".post_id={$args['post_id']})";
			}
		} elseif ( $args['post_id'] === null || $args['post_id'] === '0' || $args['post_id'] === 0 ) {
			// if not false, we search for all events which do not have a post ID (recurrences)
			$conditions['post_id'] = "(".EM_EVENTS_TABLE.".post_id IS NULL )";
		}
		// event locations
		if( !empty($args['event_location_type']) ){
			$event_location_types = explode(',', $args['event_location_type']);
			$event_locations_search = array();
			// generate array of clean and enabled event location types
			foreach( $event_location_types as $event_location_type ){
				$event_location_type = trim($event_location_type);
				if( Event_Locations::is_enabled($event_location_type) ){
					$event_locations_search[] = $event_location_type;
				}
			}
			// add condition if at least one valid/clean type supplied
			if( !empty($event_locations_search) ){
				if( count($event_locations_search) === 1 ){
					$event_location = current($event_locations_search);
					$conditions['event_location'] = "event_location_type='$event_location'";
				}else{
					$conditions['event_location'] = "event_location_type IN ('". implode("','", $event_locations_search) ."')";
				}
			}
		}
		if( isset($args['has_event_location']) && $args['has_event_location'] !== false ){
			if( $args['has_event_location'] ){
				$conditions['has_event_location'] = "event_location_type IS NOT NULL";
			}else{
				$conditions['has_event_location'] = "event_location_type IS NULL";
			}
		}
		// active status filters
		if ( em_get_option('dbem_event_status_enabled') ) {
			$active_statuses = array( 'include' => array(), 'exclude' => array() );
			// get individual status search args
			$active_status_map = array('active' => 1, 'cancelled' => 0 );
			foreach ( $active_status_map as $status_opt => $status ){
				if ( $args[$status_opt] == 1 ) {
					$active_statuses['include'][] = absint($status);
				} elseif( $args[$status_opt] !== null ) {
					$active_statuses['exclude'][] = absint($status);
				}
			}
			// get general active status
			if ( !empty($args['active_status']) ){
				$active_status_array = is_array($args['active_status']) ? $args['active_status'] : explode(',', str_replace(' ', '', $args['active_status']));
				foreach ( $active_status_array as $status ){
					if ( $status > 0 ) {
						$active_statuses['include'][] = absint($status);
					} else {
						$active_statuses['exclude'][] = absint($status);
					}
				}
			}
			// add conditional
			if( !empty($active_statuses['include']) ){
				$conditions['active_status_include'] = "event_active_status IN (". implode(',', $active_statuses['include']) .")";
			}
			if( !empty($active_statuses['exclude']) ){
				$conditions['active_status_exclude'] = "event_active_status NOT IN (". implode(',', $active_statuses['exclude']) .")";
			}
		}
		if ( $args['timeslots'] ) {
			$conditions['timeslots'] = "( timeslot_status != 0 OR timeslot_status IS NULL )";
		}
		return apply_filters( 'em_events_build_sql_conditions', $conditions, $args );
	}
	
	public static function get_sql_accepted_fields() {
		$EM_Event = new EM_Event();
		$EM_Location = new EM_Location();
		$event_fields = array_keys($EM_Event->fields);
		$location_fields = array(); //will contain location-specific fields, not ambiguous ones
		foreach( array_keys($EM_Location->fields) as $field_name ){
			if( !in_array($field_name, $event_fields) ) $location_fields[] = $field_name;
		}
		if( em_get_option('dbem_locations_enabled') ){
			$accepted_fields = array_merge($event_fields, $location_fields);
		}else{
			//if locations disabled then we don't accept location-specific fields
			$accepted_fields = $event_fields;
		}
		$accepted_fields[] = 'event_date_modified';
		$accepted_fields[] = 'event_date_created';
		return $accepted_fields;
	}
	
	/**
	 * Overrides EM_Object method to clean ambiguous fields and apply a filter to result.
	 * @see EM_Object::build_sql_orderby()
	 */
	public static function build_sql_orderby( $args, $accepted_fields, $default_order = 'ASC' ){
	    $orderby = parent::build_sql_orderby($args, $accepted_fields, em_get_option('dbem_events_default_order'));
		$orderby = self::build_sql_ambiguous_fields_helper($orderby); //fix ambiguous fields
		if ( $args['timeslots'] ) {
			foreach ( $orderby as $key => $value ) {
				$orderby[$key] = str_replace( ['event_start_date', 'event_start_time', 'event_start'], 'event_start, timeslot_start', $value );
				$orderby[$key] = str_replace( ['event_end_date', 'event_end_time', 'event_end'], 'event_end, timeslot_end', $orderby[$key] );

				if ( str_contains( $value, 'timeslot_start' ) || str_contains( $value, 'timeslot_start' ) ) {
					if ( !empty( $switched ) ) {
						unset( $orderby[ $key ] );
					} else {
						$switched = true;
					}
				}
			}
		}
		return apply_filters( 'em_events_build_sql_orderby', $orderby, $args, $accepted_fields, $default_order );
	}
	
	/**
	 * Overrides EM_Object method to clean ambiguous fields and apply a filter to result.
	 * @see EM_Object::build_sql_groupby()
	 */
	public static function build_sql_groupby( $args, $accepted_fields, $groupby_order = false, $default_order = 'ASC' ){
		$groupby = parent::build_sql_groupby($args, $accepted_fields);
		//fix ambiguous fields and give them scope of events table
		$groupby = self::build_sql_ambiguous_fields_helper($groupby);
		return apply_filters( 'em_events_build_sql_groupby', $groupby, $args, $accepted_fields );
	}
	
	/**
	 * Overrides EM_Object method to clean ambiguous fields and apply a filter to result.
	 * @see EM_Object::build_sql_groupby_orderby()
	 */
	public static function build_sql_groupby_orderby($args, $accepted_fields, $default_order = 'ASC' ){
	    $group_orderby = parent::build_sql_groupby_orderby($args, $accepted_fields, em_get_option('dbem_events_default_order'));
		//fix ambiguous fields and give them scope of events table
		$group_orderby = self::build_sql_ambiguous_fields_helper($group_orderby);
		return apply_filters( 'em_events_build_sql_groupby_orderby', $group_orderby, $args, $accepted_fields, $default_order );
	}
	
	/**
	 * Overrides EM_Object method to provide specific reserved fields and events table.
	 * @see EM_Object::build_sql_ambiguous_fields_helper()
	 */
	protected static function build_sql_ambiguous_fields_helper( $fields, $reserved_fields = array(), $prefix = 'table_name' ){
		//This will likely be removed when PHP 5.3 is the minimum and LSB is a given
		return parent::build_sql_ambiguous_fields_helper($fields, array('post_id', 'location_id', 'blog_id'), EM_EVENTS_TABLE);
	}
	
	/* 
	 * Adds custom Events search defaults
	 * @param array $array_or_defaults may be the array to override defaults
	 * @param array $array
	 * @return array
	 * @uses EM_Object#get_default_search()
	 */
	public static function get_default_search( $array_or_defaults = array(), $array = array() ){
		$defaults = array(
			'recurring' => false, //we don't initially look for recurring events only events and recurrences of recurring events
			'recurrence_set' => false, // get a specific set of event recurrences within a recurrence
			'orderby' => em_get_option('dbem_events_default_orderby'),
			'order' => em_get_option('dbem_events_default_order'),
			'groupby' => false,
			'groupby_orderby' => 'event_start_date, event_start_time', //groups according to event start time, i.e. by default shows earliest event in a scope
			'groupby_order' => 'ASC', //groups according to event start time, i.e. by default shows earliest event in a scope
			'status' => 1, //approved events only
			'town' => false,
			'state' => false,
			'country' => false,
			'region' => false,
			'postcode' => false,
			'blog' => get_current_blog_id(),
			'private' => current_user_can('read_private_events'),
			'private_only' => false,
			'post_id' => false,
			//ouput_grouped specific arguments
			'mode' => false,
			'header_format' => false,
			'date_format' => false,
			//event-specific search attributes
			'has_location' => false, //search events with a location
			'no_location' => false, //search events without a location
			'location_status' => false, //search events with locations of a specific publish status
			'event_location_type' => false,
			'has_event_location' => false,
			'cancelled' => em_get_option('dbem_events_include_status_cancelled') ? null : false, // include cancelled events
			'active' => null,
			'active_status' => null,
			'event_type' => false,
			'event_archetype' => Archetypes::get_current(), // override the default false
			'timeslots' => false, // split results by timeslots
		);
		// if event_archetype is not set, we
		//sort out whether defaults were supplied or just the array of search values
		if( empty($array) ){
			$array = $array_or_defaults;
		}else{
			$defaults = array_merge($defaults, $array_or_defaults);
		}
		//specific functionality
		if( EM_MS_GLOBAL && (!is_admin() || defined('DOING_AJAX')) ){
			if( empty($array['blog']) && is_main_site() && get_site_option('dbem_ms_global_events') ){
			    $array['blog'] = false;
			}
		}
		//admin-area specific modifiers
		if( is_admin() && !defined('DOING_AJAX') ){
			//figure out default owning permissions
			$defaults['owner'] = !current_user_can('edit_others_events') ? get_current_user_id() : false;
			if( !array_key_exists('status', $array) && current_user_can('edit_others_events') ){
				$defaults['status'] = false; //by default, admins see pending and live events
			}
		}
		//check if we're doing any location-specific searching, if so then we (by default) want to match the status of events
		if( !empty($array['has_location']) ){
			//we're looking for events with locations, so we match the status we're searching for events unless there's an argument passed on for something different
			$defaults['location_status'] = true;
		}elseif( !empty($array['no_location']) ){
			//if no location is being searched for, we should ignore any status searches for location
			$defaults['location_status'] = $array['location_status'] = false;
		}else{
			$location_specific_args = array('town', 'state', 'country', 'region', 'near', 'geo');
			foreach( $location_specific_args as $location_arg ){
				if( !empty($array[$location_arg]) ) $defaults['location_status'] = true;
			}
		}
		$args = parent::get_default_search($defaults,$array);
		//do some post-parnet cleaning up here if locations are enabled or disabled
		if( !em_get_option('dbem_locations_enabled') ){
			//locations disabled, wipe any args to do with locations so they're ignored
			$location_args = array('town', 'state', 'country', 'region', 'has_location', 'no_location', 'location_status', 'location', 'geo', 'near', 'location_id');
			foreach( $location_args as $arg ) $args[$arg] = false;
		}
		// cancelled status
		$args['cancelled'] === null ? em_get_option('dbem_events_include_status_cancelled') : $args['cancelled'];
		return apply_filters('em_events_get_default_search', $args, $array, $defaults);
	}
}
?>