<?php

/**
 *@package event-functions
 */
/**
* This functions updates a post of event type, with data given in the $post_data
* and event data given in $event_data. Returns the post_id.
*
* Triggers {@see `eventorganiser_save_event`} passing event (post) ID
*
* The event data array can contain
*
* * `schedule` => (custom | once | daily | weekly | monthly | yearly)  -- specifies the recurrence pattern
* * `schedule_meta` =>
*   * For monthly schedules,
*      * (string) BYMONTHDAY=XX to repeat on XXth day of month, e.g. BYMONTHDAY=01 to repeat on the first of every month.
*      * (string) BYDAY=ND. N= 1|2|3|4|-1 (first, second, third, fourth, last). D is day of week SU|MO|TU|WE|TH|FR|SA. E.g. BYDAY=2TU (repeat on second tuesday)
*   * For weekly schedules,
*      * (array) Days to repeat on: (SU,MO,TU,WE,TH,FR,SA). e.g. set to array('SU','TU') to repeat on Tuesdays & Sundays.
*      * Can be left blank to repeat weekly from the start date.
* * `frequency` => (int) positive integer, sets frequency of recurrence (every 2 days, or every 3 days etc)
* * `all_day` => 1 if its an all day event, 0 if not
* * `start` =>  start date (of first occurrence)  as a datetime object
* * `end` => end date (of first occurrence)  as a datetime object
* * `until` =>  **START** date of last occurrence (or upper-bound thereof) as a datetime object
* * `schedule_last` =>  Alias of until. Deprecated 2.13.0, use until.
* * `number_occurrences` => Instead of specifying `until` you can specify the number of occurrence a recurring event should have.
* This is only used if `until` is not, and for daily, weekly, monthly or yearly recurring events.
* * `include` => array of datetime objects to include in the schedule
* * `exclude` => array of datetime objects to exclude in the schedule
*
* @since 1.5
* @uses wp_insert_post()
*
* @param int $post_id - the event (post) ID for the event you want to update
* @param array $event_data - array of event data
* @param array $post_data - array of data to be used by wp_update_post.
* @return int $post_id - the post ID of the updated event
*/
function eo_update_event( $post_id, $event_data = array(), $post_data = array() ){

	$post_id = (int) $post_id;

	$input = array_merge( $post_data, $event_data );

	//Backwards compat:
	if( !empty( $input['venue'] ) ){
		$input['tax_input']['event-venue'] = $input['venue'];
	}
	if( !empty( $input['category'] ) ){
		$input['tax_input']['event-category'] = $input['category'];
	}

	$event_keys = array_flip( array(
		'start', 'end', 'schedule', 'schedule_meta', 'frequency',
		'all_day', 'until', 'schedule_last', 'include', 'exclude', 'occurs_by', 'number_occurrences',
	) );

	$post_keys = array_flip( array(
		'post_title','post_content','post_status', 'post_type','post_author','ping_status','post_parent','menu_order',
		'to_ping', 'pinged', 'post_password', 'guid', 'post_content_filtered', 'post_excerpt', 'import_id', 'tax_input',
		'comment_status', 'context', 'post_date', 'post_date_gmt',
	) );

	$event_data = array_intersect_key( $input, $event_keys );
	$post_data = array_intersect_key( $input, $post_keys ) + $post_data;

	if( empty( $post_id ) ){
		return new WP_Error( 'eo_error', 'Empty post ID.' );
	}

	/**
	 *@ignore
	 */
	$event_data = apply_filters( 'eventorganiser_update_event_event_data', $event_data, $post_id, $post_data, $event_data );
	/**
	 *@ignore
	 */
	$post_data = apply_filters( 'eventorganiser_update_event_post_data', $post_data, $post_id, $post_data, $event_data );

	if( !empty($post_data) ){
		$post_data['ID'] = $post_id;
		wp_update_post( $post_data );
	}

	/**
	 * Backwards compatability.
	 * See https://github.com/stephenharris/Event-Organiser/issues/259
	 */
	if( isset( $event_data['schedule_last'] ) ){
		if( !isset( $event_data['until'] ) ){
			$event_data['until'] = $event_data['schedule_last'];
		}
		unset( $event_data['schedule_last'] );
	}

	//Get previous data, parse with data to be updated
	$prev = eo_get_event_schedule( $post_id );
	$event_data = wp_parse_args( $event_data, $prev );

	//If schedule is 'once' and dates are included - set to 'custom':
	if( ( empty($event_data['schedule']) || 'once' == $event_data['schedule'] ) && !empty($event_data['include']) ){
		$event_data['schedule'] = 'custom';
	}

	$event_data = _eventorganiser_generate_occurrences( $event_data );

	if( is_wp_error( $event_data ) ){
		return $event_data;
	}

	//Insert new dates, remove old dates and update meta
	$re = _eventorganiser_insert_occurrences( $post_id, $event_data );

	/**
	 * Triggered after an event has been updated.
	 *
	 * @param int $post_id The ID of the event
	 */
	do_action( 'eventorganiser_save_event', $post_id );

	/**
	 * Fires after an event has been updated.
	 *
	 * @param int $post_id The ID of the event.
	 */
	do_action( 'eventorganiser_updated_event', $post_id );

	return $post_id;
}


/**
* This functions inserts a post of event type, with data given in the $post_data
* and event data given in $event_data. Returns the post ID.
*
* Triggers {@see `eventorganiser_save_event`} passing event (post) ID
*
* The event data array can contain
*
* * `schedule` => (custom | once | daily | weekly | monthly | yearly)  -- specifies the recurrence pattern
* * `schedule_meta` =>
*   * For monthly schedules,
*      * (string) BYMONTHDAY=XX to repeat on XXth day of month, e.g. BYMONTHDAY=01 to repeat on the first of every month.
*      * (string) BYDAY=ND. N= 1|2|3|4|-1 (first, second, third, fourth, last). D is day of week SU|MO|TU|WE|TH|FR|SA. E.g. BYDAY=2TU (repeat on second tuesday)
*   * For weekly schedules,
*      * (array) Days to repeat on: (SU,MO,TU,WE,TH,FR,SA). e.g. set to array('SU','TU') to repeat on Tuesdays & Sundays.
*      * Can be left blank to repeat weekly from the start date.
* * `frequency` => (int) positive integer, sets frequency of recurrence (every 2 days, or every 3 days etc)
* * `all_day` => 1 if its an all day event, 0 if not
* * `start` =>  start date (of first occurrence)  as a datetime object
* * `end` => end date (of first occurrence)  as a datetime object
* * `until` =>  **START** date of last occurrence (or upper-bound thereof) as a datetime object
* * `schedule_last` =>  Alias of until. Deprecated 2.13.0, use until.
* * `number_occurrences` => Instead of specifying `until` you can specify the number of occurrence a recurring event should have.
* This is only used if `until` is not, and for daily, weekly, monthly or yearly recurring events.
* * `include` => array of datetime objects to include in the schedule
* * `exclude` => array of datetime objects to exclude in the schedule
*
* ### Example
* The following example creates an event which starts on the 3rd December 2012 15:00 and ends on the 4th December 15:00 and repeats every 4 days until the 25th December (So the last occurrence actually ends on the 23rd).
* <code>
*     $event_data = array(
*	     'start'     => new DateTime('2012-12-03 15:00', eo_get_blog_timezone() ),
*	     'end'       => new DateTime('2012-12-04 15:00', eo_get_blog_timezone() ),
*	     'until'     => new DateTime('2012-12-25 15:00', eo_get_blog_timezone() ),
*	     'frequency' => 4,
*	     'all_day'   => 0,
*	     'schedule'  => 'daily',
*    );
*     $post_data = array(
*	     'post_title'=>'The Event Title',
*	     'post_content'=>'My event content',
*    );
*
*    $e = eo_insert_event($post_data,$event_data);
* </code>
*
* ### Tutorial
* See this <a href="http://www.stephenharris.info/2012/front-end-event-posting/">tutorial</a> or <a href="https://gist.github.com/3867194">this Gist</a> on front-end event posting.
*
* @since 1.5
* @link http://www.stephenharris.info/2012/front-end-event-posting/ Tutorial on front-end event posting
* @uses wp_insert_post()
*
* @param array $post_data array of data to be used by wp_insert_post.
* @param array $event_data array of event data
* @return int the post ID of the updated event
*/
function eo_insert_event( $post_data = array(), $event_data = array() ){
	global $wpdb;

	$input = array_merge( $post_data, $event_data );

	//Backwards compat:
	if( !empty( $input['venue'] ) ){
		$input['tax_input']['event-venue'] = $input['venue'];
	}
	if( !empty( $input['category'] ) ){
		$input['tax_input']['event-category'] = $input['category'];
	}

	$event_keys = array_flip( array(
		'start', 'end', 'schedule', 'schedule_meta', 'frequency', 'all_day',
		'until', 'schedule_last', 'include', 'exclude', 'occurs_by', 'number_occurrences',
	) );

	$post_keys = array_flip( array(
		'post_title','post_content','post_status', 'post_type','post_author','ping_status','post_parent','menu_order',
		'to_ping', 'pinged', 'post_password', 'guid', 'post_content_filtered', 'post_excerpt', 'import_id', 'tax_input',
		'comment_status', 'context',  'post_date', 'post_date_gmt',
	) );

	$event_data = array_intersect_key( $input, $event_keys ) + $event_data;
	$post_data = array_intersect_key( $input, $post_keys );

	//If schedule is 'once' and dates are included - set to 'custom':
	if( ( empty($event_data['schedule']) || 'once' == $event_data['schedule'] ) && !empty($event_data['include']) ){
		$event_data['schedule'] = 'custom';
	}

	if( !empty( $event_data['schedule_last'] ) ){
		if( !isset( $event_data['until'] ) ){
			$event_data['until'] = clone $event_data['schedule_last'];
		}
		unset( $event_data['schedule_last'] );
	}

	$event_data = _eventorganiser_generate_occurrences( $event_data );

	if( is_wp_error( $event_data ) ){
		return $event_data;
	}

	/**
	 *@ignore
	 */
	$event_data = apply_filters( 'eventorganiser_insert_event_event_data', $event_data, $post_data, $event_data );

	/**
	 *@ignore
	 */
	$post_data = apply_filters( 'eventorganiser_insert_event_post_data', $post_data, $post_data, $event_data );

	//Finally we create event (first create the post in WP)
	$post_input = array_merge(array('post_title'=>'untitled event'), $post_data, array('post_type'=>'event'));
	$post_id = wp_insert_post($post_input, true);

	//Did the event insert correctly?
	if ( is_wp_error( $post_id) )
			return $post_id;

	_eventorganiser_insert_occurrences($post_id, $event_data);

	//Action used to break cache & trigger Pro actions (& by other plug-ins?)
	/**
	 * Triggered after an event has been updated.
	 *
	 * @param int $post_id The ID of the event
	 */
	do_action( 'eventorganiser_save_event', $post_id );

	/**
	 * Fires after an event has been created.
	 *
	 * @param int $post_id The ID of the event.
	 */
	do_action( 'eventorganiser_created_event', $post_id );

	return $post_id;
}


function _eventorganiser_maybe_duplicate_post( $new_post_id, $old_post ){

	if( 'event' == get_post_type( $new_post_id ) ){
		eo_update_event( $new_post_id, eo_get_event_schedule( $old_post->ID ) );
	}

}
add_action( 'dp_duplicate_post', '_eventorganiser_maybe_duplicate_post', 50, 2 );

/**
 * Deletes all occurrences for an event (removes them from the eo_events table).
 * Triggers {@see `eventorganiser_delete_event`} (this action is used to break the caches).
 *
 * This function does not update any of the event schedule details.
 * **Don't call this unless you know what you're doing**.
 *
 * @since 1.5
 * @access private
 * @param int $post_id the event's (post) ID to be deleted
 * @param int|array $occurrence_ids Occurrence ID (or array of IDs) for specificaly occurrences to delete. If empty/false, deletes all.
 *
 */
function eo_delete_event_occurrences( $event_id, $occurrence_ids = false ){
	global $wpdb;
	//TODO use this in break/remove occurrence

	//Let's just ensure empty is cast as false
	$occurrence_ids = ( empty( $occurrence_ids ) ? false : $occurrence_ids );

	if( $occurrence_ids !== false ){
		$occurrence_ids = (array) $occurrence_ids;
		$occurrence_ids = array_map( 'absint', $occurrence_ids );
		$occurrence_ids_in = implode( ', ', $occurrence_ids );

		$raw_sql = "DELETE FROM $wpdb->eo_events WHERE post_id=%d AND event_id IN( $occurrence_ids_in )";

	}else{
		$raw_sql = "DELETE FROM $wpdb->eo_events WHERE post_id=%d";
	}

	/**
	 * @ignore
	 */
	do_action( 'eventorganiser_delete_event', $event_id, $occurrence_ids ); //Deprecated - do not use!

	/**
	 * Triggers just before the specified occurrences for the event are deleted.
	 *
	 * @param int $event_id The (post) ID of the event of which we're deleting occurrences.
	 * @param array|false $occurrence_ids An array of occurrences to be delete. If `false`, all occurrences are to be removed.
	 */
	do_action( 'eventorganiser_delete_event_occurrences', $event_id, $occurrence_ids );

	$del = $wpdb->get_results( $wpdb->prepare(  $raw_sql, $event_id ) );

}
add_action( 'delete_post', 'eo_delete_event_occurrences', 10 );

/**
* This is a private function - handles the insertion of dates into the database. Use eo_insert_event or eo_update_event instead.
* @access private
* @ignore
*
* @param int $post_id The post ID of the event
* @param array $event_data Array of event data, including schedule meta (saved as post meta), duration and occurrences
* @return int $post_id
*/
function _eventorganiser_insert_occurrences( $post_id, $event_data ) {

	global $wpdb;

	$tz = eo_get_blog_timezone();

	$start       = $event_data['start'];
	$end         = $event_data['end'];
	$occurrences = $event_data['occurrences'];

	//Don't use date_diff (requires php 5.3+)
	//Also see https://github.com/stephenharris/Event-Organiser/issues/205
	//And https://github.com/stephenharris/Event-Organiser/issues/224
	$duration_str = eo_date_interval( $start, $end, '+%a days +%h hours +%i minutes +%s seconds' );

	$event_data['duration_str'] = $duration_str;

	$schedule_last_end = clone $event_data['schedule_last'];
	$schedule_last_end->modify( $duration_str );

	//Get dates to be deleted / added
	$current_occurrences = eo_get_the_occurrences( $post_id );
	$current_occurrences = $current_occurrences ? $current_occurrences : array();

	$delete   = array_udiff( $current_occurrences, $occurrences, '_eventorganiser_compare_dates' );
	$insert   = array_udiff( $occurrences, $current_occurrences, '_eventorganiser_compare_dates' );
	$update   = array_uintersect( $occurrences, $current_occurrences, '_eventorganiser_compare_dates' );
	$update_2 = array_uintersect( $current_occurrences, $update, '_eventorganiser_compare_dates' );
	$keys     = array_keys( $update_2 );

	if ( $delete ) {
		$delete_occurrence_ids = array_keys( $delete );
		eo_delete_event_occurrences( $post_id, $delete_occurrence_ids );
	}

	$occurrence_cache = array();
	$occurrence_array = array();

	if ( $update ) {
		$update = array_combine( $keys, $update );

		foreach ( $update as $occurrence_id => $occurrence ) {

			$occurrence_end = clone $occurrence;
			$occurrence_end->modify( $duration_str );

			$occurrence_input = array(
				'StartDate'        => $occurrence->format( 'Y-m-d' ),
				'StartTime'        => $occurrence->format( 'H:i:s' ),
				'EndDate'          => $occurrence_end->format( 'Y-m-d' ),
				'FinishTime'       => $occurrence_end->format( 'H:i:s' ),
			);

			$wpdb->update(
				$wpdb->eo_events,
				$occurrence_input,
				array( 'event_id' => $occurrence_id )
			);

			$occurrence_array[$occurrence_id] = $occurrence->format( 'Y-m-d H:i:s' );
			$occurrence_cache[$occurrence_id] = array(
				'start' => $occurrence,
				'end'   => new DateTime( $occurrence_end->format( 'Y-m-d' ) . ' ' . $end->format( 'H:i:s' ), eo_get_blog_timezone() ),
			);
		}
	}

	if ( $insert ) {
		foreach ( $insert as $counter => $occurrence ) :
			$occurrence_end = clone $occurrence;
			$occurrence_end->modify( $duration_str );

			$occurrence_input = array(
				'post_id'          => $post_id,
				'StartDate'        => $occurrence->format( 'Y-m-d' ),
				'StartTime'        => $occurrence->format( 'H:i:s' ),
				'EndDate'          => $occurrence_end->format( 'Y-m-d' ),
				'FinishTime'       => $end->format( 'H:i:s' ),
				'event_occurrence' => $counter,
			);

			$wpdb->insert( $wpdb->eo_events, $occurrence_input );

			$occurrence_array[$wpdb->insert_id] = $occurrence->format( 'Y-m-d H:i:s' );
			$occurrence_cache[$wpdb->insert_id] = array(
				'start' => $occurrence,
				'end'   => new DateTime( $occurrence_end->format( 'Y-m-d' ) . ' ' . $end->format( 'H:i:s' ), $tz ),
			);
		endforeach;
	}

	//Set occurrence cache
	wp_cache_set( 'eventorganiser_occurrences_'.$post_id, $occurrence_cache );
	wp_cache_set( 'eventorganiser_all_occurrences_'.$post_id, $occurrence_cache );

	unset( $event_data['occurrences'] );

	if ( ! empty( $event_data['include'] ) ) {
		$event_data['include'] = array_map( 'eo_format_datetime', $event_data['include'], array_fill( 0, count( $event_data['include'] ), 'Y-m-d H:i:s' ) );
	}

	if ( ! empty( $event_data['exclude'] ) ) {
		$event_data['exclude'] = array_map( 'eo_format_datetime', $event_data['exclude'], array_fill( 0, count( $event_data['exclude'] ), 'Y-m-d H:i:s' ) );
	}

	update_post_meta( $post_id, '_eventorganiser_schedule_start_start', $start->format( 'Y-m-d H:i:s' ) );
	update_post_meta( $post_id, '_eventorganiser_schedule_start_finish', $end->format( 'Y-m-d H:i:s' ) );
	update_post_meta( $post_id, '_eventorganiser_schedule_until', $event_data['until']->format( 'Y-m-d H:i:s' ) );
	update_post_meta( $post_id, '_eventorganiser_schedule_last_start', $event_data['schedule_last']->format( 'Y-m-d H:i:s' ) );
	update_post_meta( $post_id, '_eventorganiser_schedule_last_finish', $schedule_last_end->format( 'Y-m-d H:i:s' ) );

	unset( $event_data['start'] );
	unset( $event_data['end'] );
	unset( $event_data['schedule_start'] );
	unset( $event_data['schedule_last'] );
	unset( $event_data['until'] );

	update_post_meta( $post_id, '_eventorganiser_event_schedule', $event_data );

	return $post_id;
}


/**
* Gets schedule meta from the database (post meta)
* Datetimes are converted to DateTime objects, in blog's currenty timezone
*
*  Event details include
*
* * `schedule` => (custom | once | daily | weekly | monthly | yearly)  -- specifies the recurrence pattern
* * `schedule_meta` =>
*   * For monthly schedules,
*      * (string) BYMONTHDAY=XX to repeat on XXth day of month, e.g. BYMONTHDAY=01 to repeat on the first of every month.
*      * (string) BYDAY=ND. N= 1|2|3|4|-1 (first, second, third, fourth, last). D is day of week SU|MO|TU|WE|TH|FR|SA. E.g. BYDAY=2TU (repeat on second tuesday)
*   * For weekly schedules,
*      * (array) Days to repeat on: (SU,MO,TU,WE,TH,FR,SA). e.g. set to array('SU','TU') to repeat on Tuesdays & Sundays.
* * `occurs_by` - For use with monthly schedules: how the event recurs: BYDAY or BYMONTHDAY
* * `frequency` => (int) positive integer, sets frequency of recurrence (every 2 days, or every 3 days etc)
* * `all_day` => 1 if its an all day event, 0 if not
* * `start` =>  start date (of first occurrence)  as a datetime object
* * `end` => end date (of first occurrence)  as a datetime object
* * `until` => For recurring events, the date they repeat until. Note that this may not be equal to `schedule_last` if
*              dates are included/excluded.
* * `schedule_last` =>  **START** date of last occurrence as a datetime object
* * `include` => array of datetime objects to include in the schedule
* * `exclude` => array of datetime objects to exclude in the schedule
*
* @param int $post_id -  The post ID of the event
* @return array event schedule details
*/
function eo_get_event_schedule( $post_id = 0 ){

	$post_id = (int) ( empty($post_id) ? get_the_ID() : $post_id);

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

	$event_details = get_post_meta( $post_id,'_eventorganiser_event_schedule', true );
	$event_details = wp_parse_args($event_details, array(
		'schedule'           => 'once',
		'schedule_meta'      => '',
		'number_occurrences' => 0, //Number occurrences according to recurrence rule. Not necessarily the #occurrences (after includes/excludes)
		'frequency'          => 1,
		'all_day'            => 0,
		'duration_str'       => '',
		'include'            => array(),
		'exclude'            => array(),
		'_occurrences'       => array(),
	));

	$tz = eo_get_blog_timezone();

	// Get start time
	if ( $start_datetime = get_post_meta( $post_id,'_eventorganiser_schedule_start_start', true ) ) {
		$event_details['start'] = new DateTime( $start_datetime, $tz );

	} else {
		// No start time, so set a default start time to next half-hour
		$now = new DateTime( 'now', $tz );

		$minute = $now->format( 'i' ) > 30 ? 0 : 30;

		$now->setTime( $now->format( 'G' ), $minute );

		if( 0 === $minute ){
			$now->modify( '+1 hour' );
		}

		$event_details['start'] = $now;
	}

	// Get end time
	if ( $end_datetime = get_post_meta( $post_id,'_eventorganiser_schedule_start_finish', true ) ) {
		$event_details['end'] = new DateTime( $end_datetime, $tz );

	} else {
		// No end time, so set a default end time
		$event_details['end'] = clone $event_details['start'];
		$event_details['end']->modify( '+1 hour' );
	}

	$event_details['schedule_start']  = clone $event_details['start'];
	$event_details['schedule_last']   = new DateTime( get_post_meta( $post_id,'_eventorganiser_schedule_last_start', true ), $tz );
	$event_details['schedule_finish'] = new DateTime( get_post_meta( $post_id,'_eventorganiser_schedule_last_finish', true ), $tz );

	if ( get_post_meta( $post_id,'_eventorganiser_schedule_until', true ) ) {
		$event_details['until'] = new DateTime( get_post_meta( $post_id,'_eventorganiser_schedule_until', true ), $tz );
	} else {
		$event_details['until'] = clone $event_details['schedule_last'];
		update_post_meta( $post_id, '_eventorganiser_schedule_until', $event_details['until']->format( 'Y-m-d H:i:s' ) );
	}

	if ( ! empty( $event_details['include'] ) ) {
		$event_details['include'] = array_map( 'eventorganiser_date_create', $event_details['include'] );
	}
	if( ! empty($event_details['exclude'] ) ){
		$event_details['exclude'] = array_map( 'eventorganiser_date_create', $event_details['exclude'] );
	}

	if ( 'weekly' == $event_details['schedule'] ) {
		$event_details['occurs_by'] = '';
	} elseif ( 'monthly' == $event_details['schedule'] ) {
		$bymonthday = preg_match( '/BYMONTHDAY=/', $event_details['schedule_meta'] );
		$event_details['occurs_by'] = ( $bymonthday ? 'BYMONTHDAY' : 'BYDAY' );
	} else {
		$event_details['occurs_by'] ='';
	}

	/**
	 * Filters the schedule metadata for an event (as returned by `eo_get_event_schedule()`.
	 *
	 * See documentation on `eo_get_event_schedule()` for more details.
	 *
	 * @param array $event_details Details of the event's dates and recurrence pattern
	 * @param int $post_id The ID of the event
	 */
	$event_details = apply_filters( 'eventorganiser_get_event_schedule', $event_details, $post_id );
	return $event_details;
}


/**
* This is a private function - handles the generation of occurrence dates from the schedule data
* @access private
* @ignore
*
* @param array $event_data - Array containing the event's schedule data
* @return array $event_data - Array containing the event's schedule data including 'occurrences', an array of DateTimes
*/
function _eventorganiser_generate_occurrences( $schedule ) {

	$event_defaults = array(
		'start' => '', 'end' => '', 'all_day' => 0,
		'schedule' => 'once', 'schedule_meta' => '', 'frequency' => 1, 'schedule_last' => '',
		'until' => '', 'number_occurrences' => 0, 'exclude' => array(), 'include' => array(),
	);

	$schedule = wp_parse_args( $schedule, $event_defaults );
	$start    = $schedule['start'];
	$end      = $schedule['end'];
	$until    = $schedule['until'];
	$schedule_meta = $schedule['schedule_meta'];

	$occurrences = array(); //occurrences array

	$exclude = array_filter( (array) $schedule['exclude'] );
	$include = array_filter( (array) $schedule['include'] );

	$exclude = array_udiff( $exclude, $include, '_eventorganiser_compare_datetime' );
	$include = array_udiff( $include, $exclude, '_eventorganiser_compare_datetime' );

	//White list schedule
	if ( ! in_array( $schedule['schedule'], array( 'once', 'daily', 'weekly', 'monthly', 'yearly', 'custom' ) ) ) {
		return new WP_Error( 'eo_error', __( 'Schedule not recognised.', 'eventorganiser' ) );
	}

	//Ensure event frequency is a positive integer. Else set to 1.
	$frequency          = max( absint( $schedule['frequency'] ), 1 );
	$all_day            = (int) $schedule['all_day'];
	$number_occurrences = absint( $schedule['number_occurrences'] );

	//Check dates are supplied and are valid
	if ( ! ( $start instanceof DateTime ) ) {
		return new WP_Error( 'eo_error', __( 'Start date not provided.', 'eventorganiser' ) );
	}

	if ( ! ( $end instanceof DateTime ) ) {
		$end = clone $start;
	}

	//If use 'number_occurrences' to limit recurring event, set dummy 'schedule_last' date.
	if ( ! ( $until instanceof DateTime ) && $number_occurrences && in_array( $schedule['schedule'], array( 'daily', 'weekly', 'monthly', 'yearly' ) ) ) {
		//Set dummy "last occurrance" date.
		$until = clone $start;
	} else {
		$number_occurrences = 0;
	}

	if ( 'once' == $schedule['schedule'] || ! ( $until instanceof DateTime ) ) {
		$until = clone $start;
	}

	//Check dates are in chronological order
	if ( $end < $start ) {
		return new WP_Error( 'eo_error', __( 'Start date occurs after end date.', 'eventorganiser' ) );
	}

	if ( $until < $start ) {
		return new WP_Error( 'eo_error', __( 'Schedule end date is before is before the start date.', 'eventorganiser' ) );
	}

	//Now set timezones
	$timezone = eo_get_blog_timezone();
	$start->setTimezone( $timezone );
	$end->setTimezone( $timezone );
	$until->setTimezone( $timezone );
	$hour = intval( $start->format( 'H' ) );
	$min  = intval( $start->format( 'i' ) );

	$start_days = array();
	$workaround = '';
	$icaldays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
	$weekdays = array( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' );
	$ical2day = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' );

	//Set up schedule
	switch ( $schedule['schedule'] ) :
		case 'once':
		case 'custom':
			$frequency = 1;
			$schedule_meta = '';
			$until = clone $start;
			$start_days[] = clone $start;
			$workaround = 'once';//Not strictly a workaround.
			break;

		case 'daily':
			$interval = sprintf( '+%d day', $frequency );
			$start_days[] = clone $start;
			break;

		case 'weekly':
			$schedule_meta = ( $schedule_meta ? array_filter( $schedule_meta ) : array() );
			if ( ! empty( $schedule_meta ) && is_array( $schedule_meta ) ) :
				foreach ( $schedule_meta as $day ) :
					$start_day = clone $start;
					$start_day->modify( $ical2day[$day] );
					$start_days[] = $start_day;
				endforeach;
			else :
				$schedule_meta = array( $icaldays[ $start->format( 'w' ) ] );
				$start_days[] = clone $start;
			endif;

			$interval = sprintf( '+%d week', $frequency );
			break;

		case 'monthly':
			$start_days[] = clone $start;
			$rule_value = explode( '=', $schedule_meta, 2 );
			$rule   = $rule_value[0];
			$values = ! empty( $rule_value[1] ) ? explode( ',', $rule_value[1] ) : array();//Should only be one value, but may support more in future
			$values = array_filter( $values );

			if ( 'BYMONTHDAY' == $rule ) :
				$date     = (int) $start_days[0]->format( 'd' );
				$interval = sprintf( '+%d month', $frequency );

				if ( $date >= 29 ) {
					$workaround = 'short months';    //This case deals with 29/30/31 of month
				}

				$schedule_meta = 'BYMONTHDAY='.$date;

			else :
				if ( empty( $values ) ) {
					$date    = (int) $start_days[0]->format( 'd' );
					$n       = ceil( $date / 7 ); // nth weekday of month.
					$day_num = intval( $start_days[0]->format( 'w' ) ); //0 (Sun) - 6(Sat)

				} else {
					//expect e.g. array( 2MO )
					preg_match( '/^(-?\d)([a-zA-Z]{2})/', $values[0], $matches );
					$n = (int) $matches[1];
					$day_num = array_search( $matches[2], $icaldays );//(Sun) - 6(Sat)
				}

				if ( 5 == $n ) {
					$n = -1;//If 5th, interpret it as last.
				}
				$ordinal = array( '1' => 'first', '2' => 'second', '3' => 'third' , '4' => 'fourth', '-1' => 'last' );

				if ( ! isset( $ordinal[$n] ) ) {
					return new WP_Error( 'eo_error', __( 'Invalid monthly schedule (invalid ordinal)', 'eventorganiser' ) );
				}

				$ical_day = $icaldays[$day_num];  //ical day from day_num (SU - SA)
				$day = $weekdays[$day_num];//Full day name from day_num (Sunday -Monday)
				$schedule_meta = 'BYDAY='.$n.$ical_day; //E.g. BYDAY=2MO
				$interval = $ordinal[$n].' '.$day.' of +'.$frequency.' month'; //E.g. second monday of +1 month

				//Work around for PHP <5.3
				if ( ! function_exists( 'date_diff' ) ) {
					$workaround = 'php5.2';
				}
			endif;
			break;

		case 'yearly':
			$start_days[] = clone $start;
			if ( '29-02' == $start_days[0]->format( 'd-m' ) ) {
				$workaround = 'leap year';
			}
			$interval = sprintf( '+%d year', $frequency );
			break;
	endswitch; //End $schedule['schedule'] switch

	//Now we have setup and validated the schedules - loop through and generate occurrences
	foreach ( $start_days as $index => $start_day ) :
		$current = clone $start_day;
		$occurrence_n = 0;

		switch ( $workaround ) :
			//Not really a workaround. Just add the occurrence and finish.
			case 'once':
				$current->setTime( $hour, $min );
				$occurrences[] = clone $current;
				break;

			//Loops for monthly events that require php5.3 functionality
			case 'php5.2':
				while ( $current <= $until || $occurrence_n < $number_occurrences ) :
					$current->setTime( $hour, $min );
					$occurrences[] = clone $current;
					$current = _eventorganiser_php52_modify( $current, $interval );
					$occurrence_n++;
				endwhile;
				break;

			//Loops for monthly events on the 29th/30th/31st
			case 'short months':
				$day_int = intval( $start_day->format( 'd' ) );

				//Set the first month
				$current_month = clone $start_day;
				$current_month = date_create( $current_month->format( 'Y-m-1' ) );

				while ( $current_month <= $until || $occurrence_n < $number_occurrences ) :
					$month_int = intval( $current_month->format( 'm' ) );
					$year_int  = intval( $current_month->format( 'Y' ) );

					if ( checkdate( $month_int , $day_int , $year_int ) ) {
						$current = new DateTime( $day_int . '-' . $month_int . '-' . $year_int, $timezone );
						$current->setTime( $hour, $min );
						$occurrences[] = clone $current;
						$occurrence_n++;
					}
					$current_month->modify( $interval );
				endwhile;
				break;

			//To be used for yearly events occuring on Feb 29
			case 'leap year':
				$current_year = clone $current;
				$current_year->modify( '-1 day' );

				while ( $current_year <= $until || $occurrence_n < $number_occurrences  ) :
					$is_leap_year = (int) $current_year->format( 'L' );

					if ( $is_leap_year ) {
						$current = clone $current_year;
						$current->modify( '+1 day' );
						$current->setTime( $hour, $min );
						$occurrences[] = clone $current;
						$occurrence_n++;
					}

					$current_year->modify( $interval );
				endwhile;
				break;

			default:
				while ( $current <= $until || $occurrence_n < $number_occurrences  ) :
					$current->setTime( $hour, $min );
					$occurrences[] = clone $current;
					$current->modify( $interval );
					$occurrence_n++;
				endwhile;
				break;

		endswitch;//End 'workaround' switch;
	endforeach;

	//Now schedule meta is set up and occurrences are generated.
	if ( $number_occurrences > 0 ) {
		//If recurrence is limited by #occurrences. Do that here.
		sort( $occurrences );
		$occurrences = array_slice( $occurrences, 0, $number_occurrences );
		$until = end( $occurrences );
	}

	//Cast includes/exclude to timezone
	$tz = eo_get_blog_timezone();
	if ( $include ) {
		foreach ( $include as $included_date ) {
			$included_date->setTimezone( $tz );
		}
	}
	if ( $exclude ) {
		foreach ( $exclude as $excluded_date ) {
			$excluded_date->setTimezone( $tz );
		}
	}

	//Add inclusions, removes exceptions and duplicates
	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		//Make sure 'included' dates doesn't appear in generate date
		$include = array_udiff( $include, $occurrences, '_eventorganiser_compare_datetime' );
	}
	$occurrences = array_merge( $occurrences, $include );
	$occurrences = array_udiff( $occurrences, $exclude, '_eventorganiser_compare_datetime' );
	$occurrences = _eventorganiser_remove_duplicates( $occurrences );

	//Sort occurrences
	sort( $occurrences );

	if ( empty( $occurrences ) || ! $occurrences[0] || ! ( $occurrences[0] instanceof DateTime ) ) {
		return new WP_Error( 'eo_error', __( 'Event does not contain any dates.', 'eventorganiser' ) );
	}

	$schedule_start = clone $occurrences[0];
	$schedule_last  = clone end( $occurrences );

	$_event_data = array(
		'start'          => $start,
		'end'            => $end,
		'all_day'        => $all_day,
		'schedule'       => $schedule['schedule'],
		'schedule_meta'  => $schedule_meta,
		'frequency'      => $frequency,
		'until'          => $until,
		'schedule_start' => $schedule_start,
		'schedule_last'  => $schedule_last,
		'exclude'        => $exclude,
		'include'        => $include,
		'occurrences'    => $occurrences,
	);

	/**
	 * Filters the event schedule after its dates has been generated by a given schedule.
	 *
	 * The filtered array is an array of occurrences generated from a
	 * schedule which may include:
	 *
	 * * **start** (DateTime) -  when the event starts
	 * * **end** (DateTime) - when the event ends
	 * * **all_day** (Bool) - If the event is all day or no
	 * * **all_day** (Bool) - If the event is all day or not
	 * * **schedule** (String) - One of once|weekl|daily|monthly|yearly|custom
	 * * **schedule_meta** (Array|String) - See documentation for `eo_insert_event()`
	 * * **frequency** (int) - The frequency of which the event repeats
	 * * **until** (DateTime) - date the schedule repeats until
	 * * **schedule_last** (DateTime) - date of last occurrence of event
	 * * **number_occurrences** (int) - number of times the event should repeat (if `until` is not specified).
	 * * **exclude** (array) - Array of DateTime objects  to exclude from the schedule
	 * * **include** (array) - Array of DateTime objects to include in the schedule
	 * * **occurrences** (array) - Array of DateTime objects generated from the above schedule.
	 *
	 * @param array $_event_data The event schedule with generated occurrences.
	 * @param array $event_data The original event schedule (without occurrences).
	 */
	$_event_data = apply_filters( 'eventorganiser_generate_occurrences', $_event_data, $schedule );
	return $_event_data;
}

/**
 * Generates the ICS RRULE fromthe event schedule data.
 * @access private
 * @ignore
 * @since 1.0.0
 * @package ical-functions
 *
 * @param int $post_id The event (post) ID. Uses current event if empty.
 * @return string The RRULE to be used in an ICS calendar
 */
function eventorganiser_generate_ics_rrule( $post_id = 0 ) {

	$post_id = (int) ( empty( $post_id ) ? get_the_ID() : $post_id );

	$rrule = eo_get_event_schedule( $post_id );
	if ( ! $rrule ) {
		return false;
	}

	$utc = new DateTimeZone( 'UTC' );
	$rrule['schedule_last']->setTimezone( $utc );

	$rrule_array = array(
		'FREQ'       => strtoupper( $rrule['schedule'] ),
		'INTERVAL'   => (int) $rrule['frequency'],
		'BYDAY'      => null,
		'BYMONTHDAY' => null,
		'UNTIL'      => $rrule['schedule_last']->format( 'Ymd\THis\Z' ),
	);

	switch ( $rrule['schedule'] ) :

		case 'daily':
		case 'yearly':
			//Do nothing
			break;

		case 'monthly':
			//TODO Account for possible day shifts with timezone set to UTC
			$schedule_meta = explode( '=', $rrule['schedule_meta'] );//BYMONTHDAY=XXX or BYDAY=XXX
			$rrule_array[$schedule_meta[0]] = $schedule_meta[1];
			break;

		case 'weekly':
			if ( ! eo_is_all_day( $post_id ) ) {

				$timezone = ( get_option( 'timezone_string' ) ? eo_get_blog_timezone() : false );

				if ( ! $timezone ) {
					// We are using a UTC offset.
					// Start dates are converted to UTC (@see https://github.com/stephenharris/Event-Organiser/issues/293),
					// which may cause it to shift *day*. E.g. a 10pm Monday event in UTC-4 will a Tuesday event in UTC.
					// We may need to correct the BYDAY attribute to be valid for UTC.
					$days_of_week = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );

					//Get day shift upon timezone set to UTC
					$start     = eo_get_schedule_start( DATETIMEOBJ, $post_id );
					$local_day = (int) $start->format( 'w' );
					$start->setTimezone( $utc );
					$utc_day   = (int) $start->format( 'w' );
					$diff      = $utc_day - $local_day + 7; //ensure difference is positive (should be 0, +1 or +6).

					//If there is a shift correct BYDAY
					if ( $diff ) {
						$utc_days = array();

						foreach ( $rrule['schedule_meta'] as $day ) {
							$utc_day_index = ( array_search( $day, $days_of_week ) + $diff ) % 7;
							$utc_days[] = $days_of_week[$utc_day_index];
						}
						$rrule['schedule_meta'] = $utc_days;
					}
				}
			}
			$rrule_array['BYDAY'] = implode( ',', $rrule['schedule_meta'] );
			break;
		case 'once':
		case 'custom':
		default:
			return false;
	endswitch;

	$rrule_string = '';
	foreach ( $rrule_array as $key => $value ) {
		if ( ! is_null( $value ) ) {
			$rrule_string .= "$key=$value;";
		}
	}

	return rtrim( $rrule_string, ';' );
}

function eventorganiser_ical_vtimezone( $timezone, $from, $to ) {

	$vtimezone = "BEGIN:VTIMEZONE\r\n";
	$vtimezone .= sprintf( "TZID:%s\r\n", $timezone->getName() );

	if ( version_compare( PHP_VERSION, '5.3.0' ) >= 0 && version_compare( PHP_VERSION, '8.1.0' ) < 0 ) {
		// get all transitions, and (as an estimate) an early one which we skip
		$transitions = $timezone->getTransitions( intval( $from - YEAR_IN_SECONDS ), (int) $to );

	} else {
		// Workaround for pre-5.3 and bug in PHP 8.1 (https://github.com/php/php-src/issues/7752)
		$transitions = $timezone->getTransitions();
		$transitions = array_filter($transitions, function($transition) use ($from, $to) {
			return $transition['ts'] >= intval( $from - YEAR_IN_SECONDS) && $transition['ts'] <= intval( $to );
		});
	}

	if ( ! $transitions ) {
		return '';
	}

	$tzfrom = null;
	foreach ( $transitions as $i => $trans ) {

		$pm      = $trans['offset'] >= 0 ? '+' : '-';
 		$hours   = floor( absint( $trans['offset'] ) / HOUR_IN_SECONDS ) % 24;
		$minutes = ( absint( $trans['offset'] ) - $hours * HOUR_IN_SECONDS ) / MINUTE_IN_SECONDS;

		$tzto = $pm . str_pad( $hours, 2, '0', STR_PAD_LEFT ) . str_pad( $minutes, 2, '0', STR_PAD_LEFT );

		// skip the first entry, we just want it for the TZOFFSETFROM value of the next one
		if ( $tzfrom === null ) {
			$tzfrom = $tzto;
			if ( count( $transitions ) > 1 ) {
				continue;
			}
		}

		$type = $trans['isdst'] ? 'DAYLIGHT' : 'STANDARD';
		$dt   = new DateTime( $trans['time'], $timezone );

		$vtimezone .= sprintf( "BEGIN:%s\r\n", $type );
		$vtimezone .= sprintf( "TZOFFSETFROM:%s\r\n", $tzfrom ); //needs formatting
		$vtimezone .= sprintf( "TZOFFSETTO:%s\r\n", $tzto ); //needs formatting
		$vtimezone .= sprintf( "DTSTART:%s\r\n",  $dt->format('Ymd\THis') );
		$vtimezone .= sprintf( "TZNAME:%s\r\n",  $trans['abbr'] );
		$vtimezone .= sprintf( "END:%s\r\n", $type );

		$tzfrom = $tzto;
	}

	$vtimezone .= 'END:VTIMEZONE';

	return $vtimezone;
}

/**
 * Removes a single occurrence and adds it to the event's 'excluded' dates.
 * @access private
 * @ignore
 * @since 1.5
 *
 * @param int $post_id The event (post) ID
 * @param int $event_id The event occurrence ID
 * @return bool|WP_Error True on success, WP_Error object on failure
 */
	function _eventorganiser_remove_occurrence($post_id=0, $event_id=0){
		global $wpdb;

		$remove = $wpdb->get_row($wpdb->prepare(
			"SELECT {$wpdb->eo_events}.StartDate, {$wpdb->eo_events}.StartTime
			FROM {$wpdb->eo_events}
			WHERE post_id=%d AND event_id=%d",$post_id,$event_id));

		if( !$remove )
			return new WP_Error('eo_notice', '<strong>'.__("Occurrence not deleted. Occurrence not found.",'eventorganiser').'</strong>');

		$date = trim($remove->StartDate).' '.trim($remove->StartTime);

		$event_details = get_post_meta( $post_id,'_eventorganiser_event_schedule',true);

		if( ($key = array_search($date,$event_details['include'])) === false){
			//If the date was not manually included, add it to the 'exclude' array
			$event_details['exclude'][] = $date;
		}else{
			//If the date was manually included, just remove it from the included dates
			unset($event_details['include'][$key]);
		}

		//Update post meta and delete date from events table
		update_post_meta( $post_id,'_eventorganiser_event_schedule',$event_details);
		eo_delete_event_occurrences( $post_id, $event_id );

		//Clear cache
		_eventorganiser_delete_calendar_cache();

		return true;
	}


/**
 * Updates a specific occurrence, and preserves the occurrence ID.
 *
 * Currently two occurrences cannot occupy the same date.
 *
 * @ignore
 * @access private
 * @since 2.12.0
 *
 * @param int $event_id      ID of the event whose occurrence we're moving
 * @param int $occurrence_id ID of the occurrence we're moving
 * @param DateTime $start    New start DateTime of the occurrence
 * @param DateTime $end      New end DateTime of the occurrence
 * @return bool|WP_Error True on success. WP_Error on failure.
 */
function eventorganiser_move_occurrence( $event_id, $occurrence_id, $start, $end ){

	global $wpdb;

	$old_start = eo_get_the_start( DATETIMEOBJ, $event_id, null, $occurrence_id );
	$schedule  = eo_get_event_schedule( $event_id );

	if( $start == $old_start ){
		return true;
	}

	$current_occurrences = eo_get_the_occurrences( $event_id );
	unset( $current_occurrences[$occurrence_id] );
	$current_occurrences = array_map( 'eo_format_datetime', $current_occurrences );

	if( in_array( $start->format( 'd-m-Y' ), $current_occurrences ) ){
		return new WP_Error( 'events-cannot-share-date', __( 'There is already an occurrence on this date', 'eventorganiser' ) );
	}

	//We update the date directly in the DB first so the occurrence is not deleted and recreated,
	//but simply updated.

	$wpdb->update(
		$wpdb->eo_events,
		array(
			'StartDate'  => $start->format( 'Y-m-d' ),
			'StartTime'  => $start->format( 'H:i:s' ),
			'EndDate'    => $end->format( 'Y-m-d' ),
			'FinishTime' => $end->format( 'H:i:s' ),
		),
		array( 'event_id' => $occurrence_id )
	);

	wp_cache_delete( 'eventorganiser_occurrences_'.$event_id );//Important: update DB clear cache
	wp_cache_delete( 'eventorganiser_all_occurrences_'.$event_id );//Important: update DB clear cache

	//Now update event schedule...

	//If date being removed was manually included remove it,
	//otherwise add it to exclude. Then add new date as include.
	if( false === ( $index = array_search( $old_start, $schedule['include'] ) ) ){
		$schedule['exclude'][] = $old_start;
	}else{
		unset( $schedule['include'][$index] );
	}
	$schedule['include'][] = $start;

	$re = eo_update_event( $event_id, $schedule );

	if( $re && !is_wp_error( $re ) ){
		return true;
	}

	return $re;
}
?>
