<?php

// =======================
// === Schedule Engine ===
// =======================
// ==== Version 2.5.0 ====
// =======================

if ( !defined( 'ABSPATH' ) ) exit;

// - Set Scheduler Debug Mode
// - Open Schedule Engine Class
// - Class Constructor
// === Caching ===
// - Set Current Schedule
// - Set Previous Shift
// - Set Current Shift
// - Set Next Shift
// === Schedule ===
// - Get Record Shifts
// - Generate Unique Shift ID
// - Generate Hashed GUID
// - Get All Shifts
// - Get Schedule Overrides
// - Get Current Schedule
// - Get Current Show
// - Get Previous Shift
// - Get Next Shift
// - Get Next Scheduled
// === Shift Checking ===
// - Schedule Conflict Checker
// - Shift Checker
// - New Shifts Checker
// - Validate Shift Time
// === Time Conversions ===
// - Get Now
// - Get Timezone
// - Get Timezone Code
// - Get Date Time Object
// - String To Time
// - Get Time
// - Get Timezone Options
// - Get Weekday(s)
// - Get Month(s)
// - Get Schedule Weekdays
// - Get Schedule Weekdates
// - Get Next Day
// - Get Previous Day
// - Get Next Date
// - Get Previous Date
// - Convert Hour to Time Format
// - Convert Shift to Time Format
// - Convert Show Shift
// - Convert Show Shifts
// - Convert Schedule Shifts
// === Time Translations ===
// - Get Locale
// - Get All Months
// - Get All Days
// - Get All Hours
// - Get All Minutes
// - Translate Weekday
// - Replace Weekdays
// - Translate Month
// - Replace Months
// - Translate Meridiem
// - Replace Meridiems
// - Translate Time String
// === Debugging ===
// - Write to Debug Log File

// ------------------------
// Set Scheduler Debug Mode
// ------------------------
// note: use anonymous function to prevent possible future conflicts
// 2.5.6: remove this as it is  unnecessary
/* add_action( 'plugins_loaded', function() {
	if ( !defined( 'SCHEDULE_ENGINE_DEBUG' ) ) {
		define( 'SCHEDULE_ENGINE_DEBUG', false );
	}
}); */

// --------------------------
// Open Schedule Engine Class
// --------------------------
// 2.5.0: added prefixed class to prevent future conflicts
class radio_station_schedule_engine {

	// --- set default arguments ---
	public $channel = '';
	public $context = '';
	public $locale = '';
	public $now = '';
	public $expires = 1800;

	public $debug = false;
	public $debug_log = false;
	
	// -----------------
	// Class Constructor
	// -----------------
	function __construct( $args ) {
		
		// --- set class variables ---
		if ( isset( $args['channel'] ) ) {
			$this->channel = $args['channel'];
		}
		if ( isset( $args['context'] ) ) {
			$this->context = $args['context'];
		}
		if ( isset( $args['locale'] ) ) {
			$this->locale = $args['locale'];
		} else {
			$this->locale = get_locale();
		}

		// 2.5.6: set now time once to prevent data mismatches
		if ( isset( $args['time'] ) ) {
			$this->now = $args['time'];
		} else {
			$this->now = $this->get_now();
		}
		// 2.5.6: allow passing of transient expiry times
		if ( isset( $args['expires'] ) ) {
			$this->expires = $args['expires'];
		}

		// --- check debug constants ---
		if ( defined( 'SCHEDULE_ENGINE_DEBUG' ) && SCHEDULE_ENGINE_DEBUG ) {
			$this->debug = SCHEDULE_ENGINE_DEBUG;
		} elseif ( isset( $args['debug'] ) ) {
			$this->debug = $args['debug'];
		}
			
		if ( defined( 'SCHEDULE_ENGINE_DEBUG_LOG' ) && SCHEDULE_ENGINE_DEBUG_LOG ) {
			$this->debug_log = SCHEDULE_ENGINE_DEBUG_LOG;
		} elseif ( isset( $args['debug_log'] ) ) {
			$this->debug_log = $args['debug_log'];
		}
		
		
		// --- add class actions ---
		add_action( 'schedule_engine_set_current_schedule', array( $this, 'set_current_schedule' ), 10, 5 );
		add_action( 'schedule_engine_set_previous_shift', array( $this, 'set_previous_shift' ), 10, 5 );
		add_action( 'schedule_engine_set_current_shift', array( $this, 'set_current_shift' ), 10, 5 );
		add_action( 'schedule_engine_set_next_shift', array( $this, 'set_next_shift' ), 10, 5 );

	}


	// ---------------
	// === Caching ===
	// ---------------

	// --------------------
	// Set Current Schedule
	// --------------------
	public function set_current_schedule( $current_schedule, $expires, $time, $channel, $context ) {

		// --- require a context ---
		if ( '' == $context ) {
			return;
		}

		// --- set current schedule transient ---
		$transient_key = 'current_schedule';
		if ( '' != $channel ) {
			$transient_key .= '_' . $channel;
		}	
		if ( $time ) {
			$time = (string) $time;
			$transient_key .= '_' . $time;
		}
		set_transient( $context . '_' . $transient_key, $current_schedule, $expires );
	}

	// ------------------
	// Set Previous Shift
	// ------------------
	public function set_previous_shift( $previous_shift, $expires, $time, $channel, $context ) {

		// --- require a context ---
		if ( '' == $context ) {
			return;
		}

		// --- set previous shift transient ---
		$transient_key = 'previous_shift';
		if ( '' != $channel ) {
			$transient_key .= '_' . $channel;
		}	
		if ( $time ) {
			$time = (string) $time;
			$transient_key .= '_' . $time;
		}
		set_transient( $context . '_' . $transient_key, $previous_shift, $expires );
	}

	// -----------------
	// Set Current Shift
	// -----------------
	function set_current_shift( $next_show, $expires, $time, $channel, $context ) {

		// --- require a context ---
		if ( '' == $context ) {
			return;
		}

		// --- set current shift transient ---
		$transient_key = 'current_shift';
		if ( '' != $channel ) {
			$transient_key .= '_' . $channel;
		}	
		if ( $time ) {
			$time = (string) $time;
			$transient_key .= '_' . $time;
		}
		set_transient( $context . '_' . $transient_key, $next_show, $expires );
	}

	// --------------
	// Set Next Shift
	// --------------
	public function set_next_shift( $next_shift, $expires, $time, $channel, $context ) {

		// --- require a context ---
		if ( '' == $context ) {
			return;
		}

		// --- set next shift transient ---
		$transient_key = 'next_shift';
		if ( '' != $channel ) {
			$transient_key .= '_' . $channel;
		}	
		if ( $time ) {
			$time = (string) $time;
			$transient_key .= '_' . $time;
		}
		set_transient( $context . '_' . $transient_key, $next_shift, $expires );
	}

	
	// ----------------
	// === Schedule ===
	// ----------------

	// -----------------
	// Get Record Shifts
	// -----------------
	public function get_record_shifts( $record_id, $key ) {

		// --- get show shift schedule ---
		$shifts = get_post_meta( $record_id, $key, true );
		if ( $shifts && is_array( $shifts ) && ( count( $shifts ) > 0 ) ) {
			$changed = false;
			foreach ( $shifts as $i => $shift ) {

				// --- check for unique ID length ---
				if ( strlen( $i ) != 8 ) {

					// --- generate unique shift ID ---
					unset( $shifts[$i] );
					$unique_id = $this->unique_shift_id();
					$shifts[$unique_id] = $shift;
					$changed = true;
				}
			}

			// --- update shifts to save unique ID indexes ---
			if ( $changed ) {
				update_post_meta( $record_id, $key, $shifts );
			}
		}

		return $shifts;
	}

	// ------------------------
	// Generate Unique Shift ID
	// ------------------------
	public function unique_shift_id() {

		// --- set channel / context ---
		$channel = $this->channel;
		$context = $this->context;

		if ( '' != $channel ) {
			$option_key = $context . '_' . $channel . '_shift_ids';
		} else {
			$option_key = $context . '_shift_ids';
		}

		// --- get all shift IDs ---
		$shift_ids = get_option( $option_key );
		if ( !$shift_ids ) {
			$shift_ids = array();
		}
		$unique_id = wp_generate_password( 8, false, false );
		if ( in_array( $unique_id, $shift_ids ) ) {
			while ( in_array( $unique_id, $shift_ids ) ) {
				$unique_id = wp_generate_password( 8, false, false );
			}
			$shift_ids[] = $unique_id;
		}

		// --- store the unique shift ID ---
		update_option( $option_key, $shift_ids );

		// --- return the unique ID ---
		return $unique_id;
	}

	// --------------------
	// Generate Hashed GUID
	// --------------------
	// 2.3.2: add hashing function for hashed GUID
	function get_hashed_guid( $record_id ) {

		global $wpdb;
		$query = "SELECT guid FROM " . $wpdb->posts . " WHERE ID = %d";
		// 2.5.0: fix to use prepare method on query
		$guid = $wpdb->get_var( $wpdb->prepare( $query, $record_id ) );
		if ( !$guid ) {
			$guid = get_permalink( $record_id );
		}
		$hash = md5( $guid );

		return $hash;
	}

	// --------------
	// Get All Shifts
	// --------------
	// 2.3.0: added get show shifts data grabber
	// 2.3.2: added second argument to get non-split shifts
	// 2.3.3: added time as third argument for ondemand shifts
	public function get_all_shifts( $records, $check_conflicts = true, $split = true, $time = false, $timezone = false ) {

		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- get weekdates for checking ---
		$now = $time ? $time : $this->get_now();
		$timezone = $timezone ? $timezone : $this->get_timezone();
		$today = $this->get_time( 'l', $now, $timezone );
		$weekdays = $this->get_schedule_weekdays( $today, $now, $timezone );
		$weekdates = $this->get_schedule_weekdates( $weekdays, $now, $timezone );
		$times = array(
			'now'       => $now,
			'today'     => $today,
			'weekdays'  => $weekdays,
			'weekdates' => $weekdates,
			'timezone'  => $timezone,
		);

		// --- process records into shift data ---
		$all_shifts = $this->get_shift_data( $records, $split, $times );
		$all_shifts = $this->sort_shifts( $all_shifts, $weekdays );

		// --- check shifts for conflicts ---
		if ( $check_conflicts ) {
			$all_shifts = $this->check_shifts( $all_shifts, $times );
		} else {
			// --- return raw data for other shift conflict checking ---
			return $all_shifts;
		}

		// --- sort into day shifts (starting today) ---
		$all_shifts = $this->sort_day_shifts( $all_shifts, $today, $timezone );
		$all_shifts = apply_filters( 'schedule_engine_all_shifts', $all_shifts, $records, $check_conflicts, $split, $time, $timezone, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$all_shifts = apply_filters( $context . '_all_shifts', $all_shifts, $records, $check_conflicts, $split, $time, $timezone, $channel );
		}
		return $all_shifts;
	}

	// --------------
	// Get Shift Data
	// --------------
	public function get_shift_data( $records, $split, $times ) {

		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- set time date to short names ---
		$now = $times['now'];
		$today = $times['today'];
		$weekdays = $times['weekdays'];
		$weekdates = $times['weekdates'];
		$timezone = $times['timezone'];

		// --- loop shows to get shifts ---
		// TODO: maybe use filter for setting record shift key?
		// $key = apply_filters( 'schedule_engine_shift_key', false, $context );
		$errors = $all_shifts = array();
		if ( $records && is_array( $records ) && ( count( $records ) > 0 ) ) {
			foreach ( $records as $record ) {

				// --- set short record ID ---
				$id = $record['ID'];

				// --- get record shifts ---
				// note: shifts must be already set
				$shifts = $record['shifts'];
				if ( $this->debug ) {
					$debug = 'Shifts for Record ' . $id . ': ' . print_r( $shifts, true );
					$this->debug_log( $debug );
				}

				if ( $shifts && is_array( $shifts) && ( count( $shifts ) > 0 ) ) {
					foreach ( $shifts as $i => $shift ) {

						// 2.3.3.9: set shift ID to key
						// 2.5.0: use already set key
						if ( !isset( $shift['id'] ) ) {
							$shift['id'] = $i;
						}

						// --- make sure shift has sufficient info ---
						$isdisabled = ( isset( $shift['disabled'] ) && ( 'yes' == $shift['disabled'] ) ) ? true : false;
						$shift = $this->validate_shift( $shift );

						if ( isset( $shift['disabled'] ) && ( 'yes' == $shift['disabled'] ) ) {

							// --- if it was not already disabled, add to shift errors ---
							if ( !$isdisabled ) {
								$errors[$id][] = $shift;
							}

						} else {

							// --- shift is valid so continue checking ---
							// 2.3.2: replace strtotime with to_time for timezones
							// 2.3.2: fix to conver to 24 hour format first
							// # midnight calculation
							$day = $shift['day'];
							$thisdate = $weekdates[$day];
							$midnight = $this->to_time( $thisdate . ' 23:59:59', $timezone ) + 1;
							$start = $shift['start_hour'] . ':' . $shift['start_min'] . ' ' . $shift['start_meridian'];
							$end = $shift['end_hour'] . ':' . $shift['end_min'] . ' ' . $shift['end_meridian'];
							$start_time = $this->convert_shift_time( $start );
							$start_time = $this->to_time( $thisdate . ' ' . $start_time, $timezone );
							if ( ( '11:59:59 pm' == $end ) || ( '12:00 am' == $end ) ) {
								// 2.3.2: simplify using existing midnight time
								$end_time = $midnight;
							} else {
								$end_time = $this->convert_shift_time( $end );
								$end_time = $this->to_time( $thisdate . ' ' . $end, $timezone );
							}
							$encore = ( isset( $shift['encore'] ) && ( 'on' == $shift['encore'] ) ) ? true : false;
							$updated = $record['updated'];

							if ( $split ) {

								// --- check if show goes over midnight ---
								if ( ( $end_time > $start_time ) || ( $end_time == $midnight ) ) {

									// --- set the shift time as is ---
									// 2.3.2: added date data
									$all_shifts[$day][$start_time . '.' . $id] = array(
										'ID'       => $id,
										'day'      => $day,
										'date'     => $thisdate,
										'start'    => $start,
										'end'      => $end,
										// 'show'     => $id,
										'encore'   => $encore,
										'split'    => false,
										'updated'  => $updated,
										'shift'    => $shift,
										'override' => false,
									);

								} else {

									// --- split shift for this day ---
									// 2.3.2: added date data
									$all_shifts[$day][$start_time . '.' . $id] = array(
										'ID'       => $id,
										'day'      => $day,
										'date'     => $thisdate,
										'start'    => $start,
										'end'      => '11:59:59 pm', // midnight
										// 'show'     => $id,
										'split'    => true,
										'encore'   => $encore,
										'updated'  => $updated,
										'shift'    => $shift,
										'real_end' => $end,
										'override' => false,
									);

									// --- split shift for next day ---
									// 2.3.2: added date data for next day
									$nextday = $this->get_next_day( $day );
									$nextdate = $weekdates[$nextday];
									// 2.3.2: fix midnight timestamp for sorting
									if ( strtotime( $nextdate ) < strtotime( $thisdate ) ) {
										$midnight = $this->to_time( $nextdate . ' 00:00:00', $timezone );
									}
									$all_shifts[$nextday][$midnight . '.' . $id] = array(
										'ID'         => $id,
										'day'        => $nextday,
										'date'       => $nextdate,
										'start'      => '00:00 am', // midnight
										'end'        => $end,
										// 'show'       => $id,
										'encore'     => $encore,
										'split'      => true,
										'updated'    => $updated,
										'shift'      => $shift,
										'real_start' => $start,
										'override'   => false,
									);
								}

							} else {

								// --- set the shift time as is ---
								// 2.3.2: added for non-split argument
								$all_shifts[$day][$start_time . '.' . $id] = array(
									'ID'       => $id,
									'day'      => $day,
									'date'     => $thisdate,
									'start'    => $start,
									'end'      => $end,
									// 'show'     => $id,
									'encore'   => $encore,
									'split'    => false,
									'updated'  => $updated,
									'shift'    => $shift,
									'override' => false,
								);

							}
						}
					}
				}
			}
		}

		// --- debug point ---
		if ( $this->debug ) {
			$debug = "Raw Shifts: " . print_r( $all_shifts, true ) . PHP_EOL;
			$debug .= "Shift Errors: " . print_r( $errors, true ) . PHP_EOL;
			$this->debug_log( $debug );
		}

		// --- do action to record shift error ---
		do_action( 'schedule_engine_set_shift_errors', $errors, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			do_action( $context . '_set_shift_errors', $errors, $channel );
		}

		// --- filter and return ---
		$all_shifts = apply_filters( 'schedule_engine_shift_data', $all_shifts, $split, $times, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$all_shifts = apply_filters( $context . '_shift_data', $all_shifts, $split, $channel, $times );
		}
		return $all_shifts;
	}

	// -----------
	// Sort Shifts
	// -----------
	public function sort_shifts( $all_shifts, $weekdays ) {

		// --- set channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- sort by start time for each day ---
		// note: all_shifts keys are made unique by combining start time and show ID
		// which allows them to be both be sorted and then checked for conflicts
		if ( count( $all_shifts ) > 0 ) {
			foreach ( $all_shifts as $day => $shifts ) {
				ksort( $shifts );
				$all_shifts[$day] = $shifts;
			}
		}

		// --- reorder by weekdays ---
		// 2.3.2: added for passing to shift checker
		$sorted_shifts = array();
		foreach ( $weekdays as $weekday ) {
			if ( isset( $all_shifts[$weekday] ) ) {
				$sorted_shifts[$weekday] = $all_shifts[$weekday];
			}
		}

		// --- debug point ---
		if ( $this->debug ) {
			$debug = "Sorted Shifts: " . print_r( $sorted_shifts, true );
			$this->debug_log( $debug );
		}
		
		// --- filter and return ---
		$sorted_shifts = apply_filters( 'schedule_engine_sorted_shifts', $sorted_shifts, $all_shifts, $weekdays, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			apply_filters( $context . '_sorted_shifts', $sorted_shifts, $all_shifts, $weekdays, $channel, $context );
		}
		return $sorted_shifts;
	}

	// ---------------
	// Sort Day Shifts
	// ---------------
	public function sort_day_shifts( $all_shifts, $today = false, $timezone = false ) {
		
		// --- set channel and context ---
		$channel = $this->channel;
		$context = $this->context;
		
		if ( !$today ) {
			$now = $this->get_now();
			$timezone = $this->get_timezone();
			$today = $this->get_time( 'l', $now, $timezone );
		}

		// --- shuffle shift days so today is first day ---
		// 2.3.2: use get time function for day with timezone
		// 2.5.0: removed get today here as already set
		$day_shifts = array();
		for ( $i = 0; $i < 7; $i ++ ) {
			// 2.5.0: shorten conditional logic to one line
			$day = ( 0 === $i ) ? $today : $this->get_next_day( $day );
			if ( isset( $all_shifts[$day] ) ) {
				$day_shifts[$day] = $all_shifts[$day];
			}
		}

		// --- filter and return ---
		// 2.3.3.9: changed conflicting filter name from radio_station_show_shifts
		$day_shifts = apply_filters( 'schedule_engine_day_shifts', $day_shifts, $all_shifts, $today, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$day_shifts = apply_filters( $context . '_day_shifts', $day_shifts, $all_shifts, $today, $channel );
		}
		return $day_shifts;
	}

	// --------------------------
	// Process Schedule Overrides
	// --------------------------
	// 2.3.0: added get schedule overrides data grabber
	public function process_overrides( $overrides, $start_date = false, $end_date = false, $timezone = false ) {

		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- loop overrides and get data ---
		$override_list = array();
		foreach ( $overrides as $i => $override ) {

			// echo '<span style="display:none;">Override: ' . print_r( $override, true ) . '</span>';

			$override_shifts = $override['shifts'];
			$show = $override['show'];
			// if ( !isset( $show['title'] ) ) {
				// echo '<span style="display:none;">Show! ' . print_r( $show, true ) . '</span>';	
			// }

			if ( $override_shifts && is_array( $override_shifts ) && ( count( $override_shifts ) > 0 ) )  {

				// --- loop override shifts ---
				// 2.3.3.9: loop to allow for multiple overrides
				foreach ( $override_shifts as $j => $data ) {

					if ( $this->debug ) {
						$debug = 'Override Data: ' . print_r( $data, true ) . PHP_EOL;
						$this->debug_log( $debug );
					}

					// 2.3.3.9: ignore disabled overrides
					if ( !isset( $data['disabled'] ) || ( 'yes' != $data['disabled'] ) ) {

						$date = $data['date'];
						if ( '' != $date ) {

							// 2.3.2: replace strtotime with to_time for timezones
							$date_time = $this->to_time( $date, $timezone );
							$inrange = true;

							// --- check if in specified date range ---
							if ( ( isset( $range_start_time ) && ( $date_time < $range_start_time ) )
									|| ( isset( $range_end_time ) && ( $date_time > $range_end_time ) ) ) {
								$inrange = false;
							}

							// --- add the override data ---
							if ( $inrange ) {

								// 2.3.2: get day from date directly
								// $thisday = date( 'l', $date_time );
								// 2.5.6: use get_time method instead of date()
								$day = $this->get_time( 'l', strtotime( $date ) );

								// 2.3.2: replace strtotime with to_time for timezones
								// 2.3.2: fix to conver to 24 hour format first
								$start = $data['start_hour'] . ':' . $data['start_min'] . ' ' . $data['start_meridian'];
								$end = $data['end_hour'] . ':' . $data['end_min'] . ' ' . $data['end_meridian'];
								$start_time = $this->convert_shift_time( $start );
								$end_time = $this->convert_shift_time( $end );
								$override_start_time = $this->to_time( $date . ' ' . $start_time, $timezone );
								$override_end_time = $this->to_time( $date . ' ' . $end_time, $timezone );
								// 2.3.2: fix for overrides ending at midnight
								// 2.3.3.9: fix to use standardized operator check
								if ( $override_end_time <= $override_start_time ) {
									$override_end_time = $override_end_time + ( 24 * 60 * 60 );
								}
								// TODO: allow for multiday overrides ?
								/* if ( isset( $data['multiday'] ) && ( 'yes' == $data['multiday'] ) ) {
									if ( isset( $data['enddate'] ) && ( '' != $data['enddate'] ) ) {

									}
								} */

								if ( $override_start_time < $override_end_time ) {

									// --- add the override as is ---
									$override_data = array(
										'override' => $show['id'],
										'id'       => $data['id'],
										'name'     => $show['title'],
										'slug'     => $show['slug'],
										'date'     => $date,
										'day'      => $day,
										'start'    => $start,
										'end'      => $end,
										'url'      => get_permalink( $show['id'] ),
										'split'    => false,
									);
									// 2.3.3.7: set array order by start time
									$override_list[$date][$override_start_time] = $override_data;

								} else {

									// --- split the override overnight ---
									$override_data = array(
										'override' => $show['id'],
										'id'       => $data['id'],
										'name'     => $show['title'],
										'slug'     => $show['slug'],
										'date'     => $date,
										'day'      => $day,
										'start'    => $start,
										'end'      => '11:59:59 pm',
										'real_end' => $end,
										'url'      => get_permalink( $show['id'] ),
										'split'    => true,
									);
									// 2.3.3.7: set array order by start time
									$override_list[$date][$override_start_time] = $override_data;

									// --- set the next day split shift ---
									// note: these should not wrap around to start of week
									// 2.3.2: use get next date/day functions
									// $nextday = date( 'l', $next_date_time );
									// $nextdate = date( 'Y-m-d', $next_date_time );
									// 2.5.6: fixed to use internal class methods
									$nextdate = $this->get_next_date( $date );
									$nextday = $this->get_next_day( $day );

									$override_data = array(
										'override'   => $show['id'],
										'id'         => $data['id'],
										'name'       => $show['title'],
										'slug'       => $show['slug'],
										'date'       => $nextdate,
										'day'        => $nextday,
										'real_start' => $start,
										'start'      => '00:00 am',
										'end'        => $end,
										'url'        => get_permalink( $show['id'] ),
										'split'      => true,
									);
									// 2.3.3.7: set array order by start time
									$override_list[$nextdate][$override_start_time] = $override_data;
								}
							}
						}
					}
				}
			}
		}

		// 2.3.3.7: reorder overrides by sequential times
		if ( count( $override_list ) > 0 ) {
			foreach ( $override_list as $day => $overrides ) {
				ksort( $overrides );
				$override_list[$day] = $overrides;
			}
		}

		// --- filter and return ---
		$override_list = apply_filters( 'schedule_engine_all_overrides', $override_list, $overrides, $start_date, $end_date, $timezone, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$override_list = apply_filters( $context . '_all_overrides', $override_list, $overrides, $start_date, $end_date, $timezone, $channel );
		}
		return $override_list;
	}

	// --------------------
	// Get Current Schedule
	// --------------------
	// 2.3.2: added optional time argument
	// 2.3.3.5: added optional weekstart argument
	public function get_current_schedule( $records, $overrides, $time = false, $weekstart = false, $timezone = false ) {

		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- get all show shifts ---
		// 2.3.3: also pass time to get show_shifts function
		$show_shifts = $this->get_all_shifts( $records, true, true, $time, $timezone );

		// --- get weekdates ---
		// 2.5.6: ensure time valus is separate to now value
		$now = $this->get_now();
		$time = $time ? $time : $now;
		$timezone = $timezone ? $timezone : $this->get_timezone();
		// $today = $this->get_time( 'l', $now, $timezone );
		// 2.3.3.5: add passthrough of optional week start argument
		$weekdays = $this->get_schedule_weekdays( $weekstart, $time, $timezone );
		$weekdates = $this->get_schedule_weekdates( $weekdays, $time, $timezone );
		// 2.5.0: set an array of times data
		$times = array(
			'time'      => $time,
			'timezone'  => $timezone,
			'weekdays'  => $weekdays,
			'weekdates' => $weekdates,
		);

		// 2.3.1: add empty keys to ensure overrides are checked
		foreach ( $weekdays as $weekday ) {
			if ( !isset( $show_shifts[$weekday] ) ) {
				$show_shifts[$weekday] = array();
			}
		}

		// --- debug point ---
		if ( $this->debug ) {
			$debug = 'Shifts: ' . print_r( $show_shifts, true );
			$this->debug_log( $debug );
		}

		// --- get show overrides ---
		// (from 12am this morning, for one week ahead and back)
		// 2.3.1: get start and end dates from weekdays
		// 2.3.2: use get time function with timezone
		// 2.3.3.9: pass second argument as time may not be now
		$start_date = $weekdates[$weekdays[0]];
		$end_date = $weekdates[$weekdays[6]];
		$overrides = apply_filters( 'schedule_engine_overrides', array(), $times, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$overrides = apply_filters( $context . '_overrides', $overrides, $times, $channel );
		}
		$overrides = $this->process_overrides( $overrides, $start_date, $end_date, $timezone );

		// --- debug point ---
		if ( $this->debug ) {
			$debug = "Time: " . $time . " - Date: " . date( 'd-m-Y', $time ) . PHP_EOL;
			if ( $now != $time ) {
				$debug .= "Now: " . $now . " - Date: " . date( 'd-m-Y', $now ) . PHP_EOL;
			}
			$debug .= "Week Start Date: " . $start_date . " - Week End Date: " . $end_date . PHP_EOL;
			$debug .= "Schedule Overrides: " . print_r( $overrides, true ) . PHP_EOL;
			$this->debug_log( $debug );
		}

		$show_shifts = $this->combine_shifts( $show_shifts, $overrides, $times );

		// --- filter and return ---
		// 2.3.2: added time argument to filter
		// 2.3.3: apply filter only once
		$show_shifts = apply_filters( 'schedule_engine_current_schedule', $show_shifts, $time, $weekstart, $timezone, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$show_shifts = apply_filters( $context . '_current_schedule', $show_shifts, $time, $weekstart, $timezone, $channel );
		}

		// --- cache expiry time ---
		// 2.3.2: set temporary transient if time is specified
		// 2.3.3: also set global data for current schedule
		// 2.5.6: use global expiry time fr\ir class
		$expires = $this->expires;

		// 2.5.0: do set current schedule action
		do_action( 'schedule_engine_set_current_schedule', $show_shifts, $expires, $time, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			do_action( $context . '_set_current_schedule', $show_shifts, $expires, $time, $channel );
		}

		// --- process shifts ---
		$show_shifts = $this->process_shifts( $show_shifts, $times );

		return $show_shifts;
	}

	// --------------
	// Combine Shifts
	// --------------
	public function combine_shifts( $show_shifts, $override_list, $times ) {

		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- set variables from times ---
		$time = $times['time'];
		$timezone = $times['timezone'];
		$weekdays = $times['weekdays'];
		$weekdates = $times['weekdates'];

		// --- apply overrides to the schedule ---
		$debugday = 'Monday';
		if ( isset( $_REQUEST['debug-day'] ) ) {
			$debugday = sanitize_text_field( $_REQUEST['debug-day'] );
		}
		// $done_overrides = array();
		if ( $override_list && is_array( $override_list ) && ( count( $override_list ) > 0 ) ) {
			foreach ( $show_shifts as $day => $shifts ) {

				$date = $weekdates[$day];
				if ( $this->debug ) {
					$debug = "Override Date: " . $date;
					$this->debug_log( $debug );
				}

				// 2.3.2: reset overrides for loop
				$overrides = array();
				if ( isset( $override_list[$date] ) ) {

					$overrides = $override_list[$date];
					if ( $this->debug ) {
						$debug = "Overrides for " . $day . ": " . print_r( $overrides, true );
						$this->debug_log( $debug );
					}

					// --- maybe reloop to insert any overrides before shows ---
					if ( count( $overrides ) > 0 ) {
						foreach ( $overrides as $i => $override ) {

							if ( $date == $override['date'] ) {

								// 2.3.1: added check if override already done
								// 2.3.2: replace strtotime with to_time for timezones
								// 2.3.2: fix to convert to 24 hour format first
								// 2.3.3.7: remove check if override already done from here
								$override_start = $this->convert_shift_time( $override['start'] );
								$override_end = $this->convert_shift_time( $override['end'] );
								$override_start_time = $this->to_time( $date . ' ' . $override_start, $timezone );
								$override_end_time = $this->to_time( $date . ' ' . $override_end, $timezone );
								if ( isset( $override['split'] ) && $override['split'] && ( '11:59:59 pm' == $override['end'] ) ) {
									// 2.3.3.8: fix to add 60 seconds instead of 1
									$override_end_time = $override_end_time + 60;
								}
								// 2.3.2: fix for non-split overrides ending on midnight
								// 2.3.3.9: added or equals to operator
								if ( $override_end_time <= $override_start_time ) {
									$override_end_time = $override_end_time + ( 24 * 60 * 60 );
								}

								// --- check for overlapped shift (if any) ---
								// 2.3.1 added check for shift count
								if ( count( $shifts ) > 0 ) {
									// 2.3.3.7: change shifts variable in loop not just show_shifts
									foreach ( $shifts as $start => $shift ) {

										// 2.3.2: replace strtotime with to_time for timezones
										// 2.3.2: fix to convert to 24 hour format first
										$shift_start = $this->convert_shift_time( $shift['start'] );
										$shift_end = $this->convert_shift_time( $shift['end'] );
										$start_time = $this->to_time( $date . ' ' . $shift_start, $timezone );
										$end_time = $this->to_time( $date . ' ' . $shift_end, $timezone );
										if ( isset( $shift['split'] ) && $shift['split'] && ( '11:59:59 pm' == $shift['end'] ) ) {
											// 2.3.3.8: fix to add 60 seconds instead of 1
											$end_time = $end_time + 60;
										}
										// 2.3.2: fix for non-split shifts ending on midnight
										// 2.3.3.9: added or equals to operator
										if ( $end_time <= $start_time ) {
											$end_time = $end_time + ( 24 * 60 * 60 );
										}

										if ( $day == $debugday ) {
											// 2.4.0.6: fix to undefined variable warning
											if ( !isset( $debugshifts ) ) {
												$debugshifts = '';
											}
											$debugshifts .= $day . ' Show from ' . $shift['start'] . ': ' . $start_time . ' to ' . $shift['end'] . ': ' . $end_time . PHP_EOL;
											$debugshifts .= $day . ' Override from ' . $override['start'] . ': ' . $override_start_time . ' to ' . $override['end'] . ': ' . $override_end_time . PHP_EOL;
										}

										// --- check if the override starts earlier than shift ---
										if ( $override_start_time < $start_time ) {

											// --- check when the shift ends ---
											if ( ( $override_end_time > $end_time )
												|| ( $override_end_time == $end_time ) ) {
												// --- overlaps so remove shift ---
												if ( $day == $debugday ) {
													$debugshifts .= "Removed Shift: " . print_r( $shift, true ) . PHP_EOL;
												}
												unset( $show_shifts[$day][$start] );
												unset( $shifts[$start] );
											} elseif ( $override_end_time > $start_time ) {
												// --- add trimmed shift remainder ---
												if ( $day == $debugday ) {
													$debugshifts .= "Trimmed Start of Shift to " . $override['end'] . ": " . print_r( $shift, true ) . PHP_EOL;
												}
												unset( $show_shifts[$day][$start] );
												unset( $shifts[$start] );
												$shift['start'] = $override['end'];
												$shift['trimmed'] = 'start';
												$shifts[$override['end']] = $shift;
												$show_shifts[$day] = $shifts;
											}

											// --- add the override if not already added ---
											// 2.3.3.8: removed adding of overrides here
											/* if ( !in_array( $override['date'] . '--' . $i, $done_overrides ) ) {
												$done_overrides[] = $override['date'] . '--' . $i;
												if ( $day == $debugday ) {
													$debugshifts .= "Added Override: " . print_r( $override, true ) . PHP_EOL;
												}
												$shifts[$override['start']] = $override;
												$show_shifts[$day] = $shifts;
											} */

										} elseif ( $override_start_time == $start_time ) {

											// --- same start so overwrite the existing shift ---
											// 2.3.1: set override done instead of unsetting override
											// 2.3.3.7: remove check if override already done
											// $done_overrides[] = $date . '--' . $i;
											if ( $day == $debugday ) {
												$debugshifts .= "Replaced Shift with Override: " . print_r( $show_shifts[$day][$start], true ) . PHP_EOL;
											}
											$shifts[$start] = $override;
											$show_shifts[$day] = $shifts;

											// --- check if there is remainder of existing show ---
											if ( $override_end_time < $end_time ) {
												$shift['start'] = $override['end'];
												$shift['trimmed'] = 'start';
												$shifts[$override['end']] = $shift;
												$show_shifts[$day] = $shifts;
												if ( $day == $debugday ) {
													$debugshifts .= "And trimmed Shift Start to " . $override['end'] . PHP_EOL;
												}
											}
											// elseif ( $override_end_time == $end_time ) {
												// --- remove exact override ---
												// do nothing, already overridden
											// }

										} elseif ( ( $override_start_time > $start_time )
												&& ( $override_start_time < $end_time ) ) {

											$end = $shift['end'];

											// --- partial shift before override ---
											if ( $day == $debugday ) {
												$debugshifts .= "Trimmed Shift End to " . $override['start'] . ": " . print_r( $shift, true ) . PHP_EOL;
											}
											$shift['start'] = $start;
											$shift['end'] = $override['start'];
											$shift['trimmed'] = 'end';
											$shifts[$start] = $shift;
											$show_shifts[$day] = $shifts;

											// --- add the override ---
											$show_shifts[$day][$override['start']] = $override;
											// 2.3.1: track done instead of unsetting
											// 2.3.3.7: remove check if override already done here
											// $done_overrides[] = $date . '--' . $i;

											// --- partial shift after override ----
											if ( $override_end_time < $end_time ) {
												if ( $day == $debugday ) {
													$debugshifts .= "And added partial Shift after " . $override['end'] . ": " . print_r( $shift, true ) . PHP_EOL;
												}
												$shift['start'] = $override['end'];
												$shift['end'] = $end;
												$shift['trimmed'] = 'start';
												$shifts[$override['end']] = $shift;
												$show_shifts[$day] = $shifts;
											}
										}
									}
								}
							}
						}
					}
				}

				// --- add directly any remaining overrides ---
				// 2.3.1: fix to include standalone overrides on days
				// 2.3.3.8: moved override adding to fix shift order
				if ( count( $overrides ) > 0 ) {
					foreach ( $overrides as $i => $override ) {
						if ( $date == $override['date'] ) {
							// 2.3.3.7: remove check if override already done
							// if ( !in_array( $date . '--' . $i, $done_overrides ) ) {
								// $done_overrides[] = $date . '--' . $i;
								$show_shifts[$day][$override['start']] = $override;
								if ( $day == $debugday ) {
									$debugshifts .= "Added Override: " . print_r( $override, true ) . PHP_EOL;
								}
							// }
						}
					}
				}

				// --- sort the shifts using 24 hour time ---
				$shifts = $show_shifts[$day];
				if ( count( $shifts ) > 0 ) {
					// 2.3.2: fix to clear shift keys between days
					$new_shifts = $shift_keys = array();
					$keys = array_keys( $shifts );
					foreach ( $keys as $i => $key ) {
						$converted = $this->convert_shift_time( $key, 24 );
						unset( $keys[$i] );
						$keys[$key] = $shift_keys[$key] = $converted;
					}
					sort( $shift_keys );
					foreach ( $shift_keys as $shift_key ) {
						if ( in_array( $shift_key, $keys ) ) {
							$key = array_search( $shift_key, $keys );
							$new_shifts[$key] = $shifts[$key];
						}
					}
					$shifts = $show_shifts[$day] = $new_shifts;

					if ( $this->debug ) {
						if ( isset( $debugshifts ) ) {
							$this->debug_log( "Day Debug: " . $debugshifts );
						}
						$debug = "Shift Keys: " . print_r( $keys, true ) . PHP_EOL;
						$debug .= "Sorted Keys: " . print_r( $shift_keys, true ) . PHP_EOL;
						$debug .= "Sorted Shifts: " . print_r( $new_shifts, true ) . PHP_EOL;
						$this->debug_log( $debug );
					}

				}

				// 2.5.6: remove assignment repetition
				// $shifts = $show_shifts[$day];
				// ksort( $shifts );
				if ( $this->debug ) {
					$debug = "New Day Shifts: " . print_r( $shifts, true );
					$this->debug_log( $debug );
				}
			}

		}

		if ( $this->debug ) {
			$debug = "Combined Schedule: " . print_r( $show_shifts, true );
			$this->debug_log( $debug );
		}
		
		return $show_shifts;
	}

	// --------------
	// Process Shifts
	// --------------
	public function process_shifts( $show_shifts, $times ) {

		// --- set channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- set variables from times ---
		$now = $this->now;
		$time = $times['time'];
		$timezone = $times['timezone'];
		$weekdays = $times['weekdays'];
		$weekdates = $times['weekdates'];

		// --- loop all shifts to check current ---
		$prev_shift = $set_prev_shift = $prev_shift_end = false;
		foreach ( $show_shifts as $day => $shifts ) {

			// 2.3.1: added check for shift count
			if ( count( $shifts ) > 0 ) {

				foreach ( $shifts as $start => $shift ) {

					// --- check if shift is an override ---
					if ( isset( $shift['override'] ) && $shift['override'] ) {

						// ---- add the override data ---
						// 2.5.6: use apply_filters instead of prefixed functions
						$override = apply_filters( 'schedule_engine_schedule_override_data_meta', $shift['override'], $shift['id'], $context );
						if ( ( '' != $context ) && is_string( $context ) ) {
							$override = apply_filters( $context . '_schedule_override_data_meta', $shift['override'], $shift['id'] );
						}
						$shift['show'] = $show_shifts[$day][$start]['show'] = $override;

					} else {
				
						// --- add show data back to shift ---
						// 2.5.6: use apply_filters instead of prefixed functions
						$show = apply_filters( 'schedule_engine_schedule_show_data_meta', $shift['ID'], $shift['id'], $context );
						if ( ( '' != $context ) && is_string( $context ) ) {
							$show = apply_filters( $context . '_schedule_show_data_meta', $shift['ID'], $shift['id'] );
						}
						unset( $show['schedule'] );
						$shift['show'] = $show_shifts[$day][$start]['show'] = $show;
					}

					if ( !isset( $current_show ) ) {

						// --- get this shift start and end times ---
						// 2.3.2: replace strtotime with to_time for timezones
						// 2.3.2: fix to convert to 24 hour format first
						$shift_start = $this->convert_shift_time( $shift['start'] );
						$shift_end = $this->convert_shift_time( $shift['end'] );
						$shift_start_time = $this->to_time( $weekdates[$day] . ' ' . $shift_start, $timezone );
						$shift_end_time = $this->to_time( $weekdates[$day] . ' ' . $shift_end, $timezone );

						// if ( isset( $shift['split'] ) && $shift['split'] && isset( $shift['real_end'] ) {
						//	$nextdate = $this->get_time( 'date', $shift_end_time + ( 23 * 60 * 60 ) );
						//	$shift_end = $nextdate[$day] . ' ' . $shift['real_end'];
						// }

						// - adjust for shifts ending past midnight -
						// 2.3.3.9: added or equals to operator
						if ( $shift_end_time <=  $shift_start_time ) {
							$shift_end_time = $shift_end_time + ( 24 * 60 * 60 );
						}

						// --- check if this is the currently scheduled show ---
						if ( ( $now >= $shift_start_time ) && ( $now < $shift_end_time ) ) {

							if ( isset( $maybe_next_show ) ) {
								unset( $maybe_next_show );
							}
							$shift['day'] = $day;
							$current_show = $shift;

							// 2.3.3: set current show to global data
							// 2.3.4: set previous show shift to global and transient
							// 2.3.3.8: move expires declaration earlier
							// 2.5.6: fallback to global expiration time
							$expires = $shift_end_time - $now - 1;
							if ( $expires > $this->expires ) {
								$expires = $this->expires;
							}
							
							// 2.5.0: do set current shift action
							do_action( 'schedule_engine_set_current_shift', $current_show, $expires, $time, $channel, $context );
							if ( ( '' != $context ) && is_string( $context ) ) {
								do_action( $context . '_set_current_shift', $current_show, $expires, $time, $channel );
							}

						} elseif ( $now > $shift_end_time ) {

							// 2.3.2: set previous shift flag
							$set_prev_shift = true;

						} elseif ( ( $now < $shift_start_time ) && !isset( $maybe_next_show ) ) {

							// 2.3.2: set maybe next show
							$maybe_next_show = $shift;

						}

						// --- debug point ---
						if ( $this->debug ) {
							$debug = 'Time: ' . $time . ' - Date: ' . $this->get_time( 'm-d H:i:s', $time ) . PHP_EOL;
							if ( $time != $now ) {
								$debug .= 'Now: ' . $now . ' - Date: ' . $this->get_time( 'm-d H:i:s', $now ) . PHP_EOL;
							}
							$debug .= 'Shift Start: ' . $shift_start . ' (' . $shift_start_time . ')' . PHP_EOL;
							$debug .= 'Shift End: ' . $shift_end . ' (' . $shift_end_time . ')' . PHP_EOL;
							if ( isset ( $current_show ) ) {
								$debug .= '[Current Shift] ' . print_r( $current_show, true ) . PHP_EOL;
							}
							if ( $now >= $shift_start_time ) {$debug .= "!A!";}
							if ( $now < $shift_end_time ) {$debug .= "!B!";}
							$this->debug_log( $debug );
						}

					} elseif ( isset( $current_show['split'] ) && $current_show['split'] ) {

						// --- skip second part of split shift for current shift ---
						// (so that it is not set as the next show)
						unset( $current_show['split'] );

					}

					// 2.3.2: change to logic to allow for no current show found
					if ( !isset( $next_show ) ) {

						// --- get shift times ---
						// 2.3.2: replace strtotime with to_time for timezones
						// 2.3.2: fix to convert to 24 hour format first
						// 2.5.6: fix start_time and end_time to shift_start and shift_end
						$shift_start = $this->convert_shift_time( $shift['start'] );
						$shift_end = $this->convert_shift_time( $shift['end'] );
						$shift_start_time = $this->to_time( $weekdates[$day] . ' ' . $shift_start, $timezone );
						$shift_end_time = $this->to_time( $weekdates[$day] . ' ' . $shift_end, $timezone );
						// 2.3.3.9: added or equals to operator
						if ( $shift_end_time <= $shift_start_time ) {
							$shift_end_time = $shift_end_time + ( 24 * 60 * 60 );
						}

						if ( isset( $current_show ) || ( $prev_shift_end && ( $now > $prev_shift_end ) && ( $now < $shift_start_time ) ) ) {

							// --- set next show ---
							// 2.3.2: set date for widget
							$next_show['date'] = $weekdates[$day];
							$next_show = $shift;

						}
					}

					// 2.3.2: maybe set previous shift end value
					if ( $set_prev_shift ) {
						$prev_shift_end = $shift_end_time;
					}

					// 2.3.4: set previous shift value
					$prev_shift = $shift;

				}
			}
		}

		// --- maybe set next show transient ---
		// 2.3.2: check for (possibly first) next show found
		if ( !isset( $next_show ) && isset( $maybe_next_show ) ) {
			$next_show = $maybe_next_show;
		}

		// 2.5.6: fix to reset time flag if same as now
		if ( $time == $now ) {
			$time = false;
		}
			
		if ( isset( $next_show ) ) {

			// 2.3.2: recombine split shift end times
			$shift = $next_show;
			if ( isset( $shift['split'] ) && $shift['split'] && isset( $shift['real_end'] ) ) {
				$next_show['end'] = $shift['real_end'];
				unset( $next_show['split'] );
			}

			// 2.3.2: added check that expires is set
			$next_expires = $shift_end_time - $now - 1;
			if ( isset( $expires ) && ( $next_expires > ( $expires + $this->expires ) ) ) {
				$next_expires = $expires + $this->expires;
			}

			// 2.5.0: do next shift record action
			// echo '<span style="display:none;">&&&'; var_dump( $time ); var_dump( $next_expires ); var_dump( $next_show ); echo '</span>';
			do_action( 'schedule_engine_set_next_shift', $next_show, $next_expires, $time, $channel, $context );
			if ( ( '' != $context ) && is_string( $context ) ) {
				do_action( $context . '_set_next_shift', $next_show, $next_expires, $time, $channel );
			}

		}

		if ( $this->debug ) {
			if ( !isset( $current_show ) ) {
				$debug = 'Current Show Not Found.';
			} else {
				$debug = 'Current Show: ' . print_r( $current_show, true );
			}
			$this->debug_log( $debug );
		}

		// --- get next show if we did not find one ---
		if ( !isset( $next_show ) ) {

			if ( $this->debug ) {
				$debug = 'No Next Show Found. Rechecking...';
				$this->debug_log( $debug );
			}

			// --- fallback to using get next shift function ---
			// 2.3.2: added time argument to next shows retrieval
			// 2.3.2: set next show transient within next shows function
			$next_shows = $this->get_next_shift( $show_shifts, $time, $timezone );

		}

		// --- debug point ---
		if ( $this->debug ) {
			$debug = '';
			if ( isset( $current_show ) ) {
				$debug .= "Current Show: " . print_r( $current_show, true ) . PHP_EOL;
			}
			if ( isset( $next_show ) ) {
				$debug .= "Next Show: " . print_r( $next_show, true ) . PHP_EOL;
			}
			$next_shows = $this->get_next_shifts( 5, $show_shifts, $time, $timezone );
			$debug .= "Next 5 Shows: " . print_r( $next_shows, true ) . PHP_EOL;
			$this->debug_log( $debug );
		}

		return $show_shifts;
	}

	// ------------------
	// Get Current Shifts
	// ------------------
	// 2.3.0: added new get current show function
	// 2.3.2: added optional time argument
	public function get_current_shifts( $schedule_shifts = false, $time = false, $timezone = false ) {

		// --- set channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		$current_shift = $previous_shift = $next_shift = false;

		// --- get all scheduled shifts ---
		// 2.5.6: disabled as get_all_shifts args incorrect
		/* if ( !$schedule_shifts ) {
			if ( !$time ) {
				$schedule_shifts = $this->get_all_shifts();
			} else {
				$schedule_shifts = $this->get_all_shifts( $time );
			}
		} */

		// --- get current time ---
		$now = $time ? $time : $this->get_now();

		// --- get schedule for time ---
		// 2.3.3: use weekday name instead of number (w)
		// 2.3.3: add fix to start from previous day
		$today = $this->get_time( 'l', $now, $timezone );
		$yesterday = $this->get_previous_day( $today );
		$weekdays = $this->get_schedule_weekdays( $yesterday );
		$weekdates = $this->get_schedule_weekdates( $weekdays, $now, $timezone );

		if ( $this->debug ) {
			$debug = 'Finding Current Show from ' . $yesterday . PHP_EOL;
			$debug .= 'Weekdays: ' . print_r( $weekdays, true ) . PHP_EOL;
			$debug .= 'Weekdates: ' . print_r( $weekdates, true ) . PHP_EOL;
			$this->debug_log( $debug );
		}

		// --- loop shifts to get current show ---
		$current_split = $prev_show = false;
		foreach ( $weekdays as $day ) {
			if ( isset( $schedule_shifts[$day] ) ) {
				$shifts = $schedule_shifts[$day];
				foreach ( $shifts as $start => $shift ) {

					// --- get this shift start and end times ---
					$shift_start = $this->convert_shift_time( $shift['start'] );
					$shift_end = $this->convert_shift_time( $shift['end'] );
					$shift_start_time = $this->to_time( $weekdates[$day] . ' ' . $shift_start, $timezone );
					$shift_end_time = $this->to_time( $weekdates[$day] . ' ' . $shift_end, $timezone );
					// 2.3.3: fix for shifts split over midnight
					// 2.3.3.9: added or equals to operator
					if ( $shift_end_time <= $shift_start_time ) {
						$shift_end_time = $shift_end_time + ( 24 * 60 * 60 );
					}

					if ( $this->debug ) {
						$debug = 'Now: ' . $now . ' - Shift Start: ' . $shift_start_time . ' - Shift End: ' . $shift_end_time . PHP_EOL;
						$debug .= 'Shift: ' . print_r( $shift, true ) . PHP_EOL;
						$this->debug_log( $debug );
					}

					// --- set current show ---
					// 2.3.3: get current show directly and remove transient
					// 2.4.0.6: fix to add equal to operator for start time
					if ( ( $now >= $shift_start_time ) && ( $now < $shift_end_time ) ) {

						if ( $this->debug ) {
							$debug = '^^^ Current ^^^';
							$this->debug_log( $debug );
						}
						// --- recombine possible split shift to set current show ---
						$current_shift = $shift;

						// 2.3.4: also set previous shift data
						// 2.5.6: added isset check for prev_shift
						if ( isset( $prev_shift ) && $prev_shift ) {
							
							$previous_shift = $prev_shift;
							$expires = $shift_end_time - $now - 1;
							
							do_action( 'schedule_engine_previous_shift', $previous_shift, $expires, $time, $channel, $context );
							if ( ( '' != $context ) && is_string( $context ) ) {
								do_action( $context . '_previous_shift', $previous_shift, $expires, $time, $channel );
							}

						}

					}

					// 2.3.4: store previous shift
					$prev_shift = $shift;
				}
			}
		}

		// --- filter current show ---
		// 2.3.2: added time argument to filter
		$current_shifts = array(
			'current'	=> $current_shift,
			'previous'  => $previous_shift,
			// 'next'		=> $next_shift,
		);
		$current_shifts = apply_filters( 'schedule_engine_current_shifts', $current_shifts, $time, $schedule_shifts, $timezone );
		return $current_shifts;
	}

	// -----------------
	// Get Current Shift
	// -----------------
	public function get_current_shift( $schedule_shifts = false, $time = false, $timezone = false ) {
		// TODO: check for cached current shift?
		$current_shifts = $this->get_current_shifts( $schedule_shifts, $time, $timezone );
		$current_shift = $current_shifts['current'];
		return $current_shift;
	}

	// ------------------
	// Get Previous Shift
	// ------------------
	public function get_previous_shift( $schedule_shifts = false, $time = false, $timezone = false ) {
		
		// TODO: check for cached previous shift?
		// 2.5.6: disabled as get_all_shifts args incorrect
		/* if ( !$schedule_shifts ) {
			if ( !$time ) {
				$schedule_shifts = $this->get_all_shifts();
			} else {
				$schedule_shifts = $this->get_all_shifts( $time );
			}
		} */

		$current_shifts = $this->get_current_shifts( $schedule_shifts, $time, $timezone );
		$previous_shift = $current_shifts['previous'];
		return $previous_shift;
	}

	// --------------
	// Get Next Shift
	// --------------
	public function get_next_shift( $schedule_shifts = false, $time = false, $timezone = false ) {

		// 2.5.6: disabled as get_all_shifts args incorrect
		/* if ( !$schedule_shifts ) {
			if ( !$time ) {
				$schedule_shifts = $this->get_all_shifts();
			} else {
				$schedule_shifts = $this->get_all_shifts( $time );
			}
		} */

		// $current_shifts = $this->get_current_shifts( $schedule_shifts, $time, $timezone );
		// $next_shift = $current_shifts['next'];
		$next_shifts = $this->get_next_shifts( 1, $schedule_shifts, $time, $timezone );
		$next_shift = ( is_array( $next_shifts ) && ( count( $next_shifts ) > 0 ) ) ? $next_shifts[0] : false;
		return $next_shift;
	}

	// ------------------
	// Get Next Scheduled
	// ------------------
	// 2.3.0: added new get next shows function
	// 2.3.2: added optional time argument
	public function get_next_shifts( $limit = 3, $scheduled_shifts = false, $time = false, $timezone = false ) {

		// --- get all show shifts ---
		// TODO: check this as may be needed to prevent an endless loop?
		// 2.5.6: disabled as get_all_shifts args incorrect
		/* if ( !$scheduled_shifts ) {
			if ( !$time ) {
				$scheduled_shifts = $this->get_all_shifts();
			} else {
				$scheduled_shifts = $this->get_all_shifts( $time );
			}
		} */

		// --- loop (remaining) shifts to add show data ---
		$next_shows = array();
		// 2.3.2: maybe set provided time as now
		$now = $time ? $time : $this->get_now();
		$timezone = $timezone ? $timezone : $this->get_timezone();

		// 2.3.2: use get time function with timezone
		// 2.3.2: fix to pass week day start as numerical (w)
		// 2.3.3: revert to passing week day start as day (l)
		// 2.3.3: added fix to start from previous day
		$today = $this->get_time( 'l', $now, $timezone );
		$yesterday = $this->get_previous_day( $today );
		$weekdays = $this->get_schedule_weekdays( $yesterday );
		$weekdates = $this->get_schedule_weekdates( $weekdays, $now, $timezone );

		if ( $this->debug ) {
			$debug = 'Next Shows from ' . $yesterday . PHP_EOL;
			$debug .= 'Weekdays: ' . print_r( $weekdays, true ) . PHP_EOL;
			$debug .= 'Weekdates: ' . print_r( $weekdates, true ) . PHP_EOL;
			$this->debug_log( $debug );
		}

		// --- loop shifts to find next shows ---
		$current_split = false;
		foreach ( $weekdays as $day ) {
			if ( isset( $scheduled_shifts[$day] ) ) {
				$shifts = $scheduled_shifts[$day];
				foreach ( $shifts as $start => $shift ) {

					// --- get this shift start and end times ---
					// 2.3.2: replace strtotime with to_time for timezones
					// 2.3.2: fix to convert to 24 hour format first
					$shift_start = $this->convert_shift_time( $shift['start'] );
					$shift_end = $this->convert_shift_time( $shift['end'] );
					$shift_start_time = $this->to_time( $weekdates[$day] . ' ' . $shift_start, $timezone );
					$shift_end_time = $this->to_time( $weekdates[$day] . ' ' . $shift_end, $timezone );

					if ( $this->debug ) {
						$debug = 'Next? ' . $now . ' - ' . $shift_start_time . ' - ' . $shift_end_time . PHP_EOL;
						$debug .= radio_station_get_time( 'date', $now ) . ' - ' . radio_station_get_time( 'date', $shift_start_time ) . ' - ' . radio_station_get_time( 'date', $shift_end_time ) . PHP_EOL;
						$debug .= 'Shift: ' . print_r( $shift, true ) . PHP_EOL;
						$this->debug_log( $debug );
					}

					// --- set current show ---
					// 2.3.2: set current show transient
					// 2.3.3: remove current show transient

					// --- check if show is upcoming ---
					if ( $now < $shift_start_time ) {

						// --- reset skip flag ---
						$skip = false;

						if ( $current_split ) {

							$skip = true;
							$current_split = false;

						} elseif ( isset( $shift['split'] ) && $shift['split'] ) {

							// --- dedupe for shifts split overnight ---
							if ( isset( $shift['real_end'] ) ) {
								$shift['end'] = $shift['real_end'];
								$current_split = true;
							} elseif ( isset( $shift['real_start'] ) ) {
								// 2.3.3: skip this shift instead of setting
								// (because second half of current show!)
								$skip = true;
							}

						} else {
							// --- reset split shift flag ---
							$current_split = false;
						}

						if ( !$skip ) {

							// --- add to next shows data ---
							// 2.3.2: set date for widget display
							$shift['date'] = $weekdates[$day];
							$next_shows[] = $shift;
							if ( $this->debug ) {
								$debug = 'Next Shows: ' . print_r( $next_shows, true );
								$this->debug_log( $debug );
							}

							// --- return if we have reached limit ---
							if ( count( $next_shows ) == $limit ) {
								$next_shows = apply_filters( 'schedule_engine_next_scheduled', $next_shows, $limit, $scheduled_shifts, $time, $timezone );
								return $next_shows;
							}
						}
					}
				}
			}
		}

		// --- filter and return ---
		$next_shows = apply_filters( 'schedule_engine_next_scheduled', $next_shows, $limit, $scheduled_shifts, $time, $timezone );
		return $next_shows;
	}


	// ----------------------
	// === Shift Checking ===
	// ----------------------

	// -------------------------
	// Schedule Conflict Checker
	// -------------------------
	// (checks all existing show shifts for schedule)
	// 2.3.0: added show shift conflict checker
	public function check_shifts( $all_shifts, $times = false ) {

		// --- set channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// TODO: check for start of week and end of week shift conflicts?

		// 2.3.3.9: fix weekday/dates code to match get_show_shifts
		if ( $times ) {
			$now = $times['now'];
			$timezone = $times['timezone'];
			$today = $times['today'];
			$weekdays = $times['weekdays'];
			$weekdates = $times['weekdates'];
		} else {
			$now = $this->get_now();
			$timezone = $this->get_timezone();
			$today = $this->get_time( 'l', $now, $timezone );
			$weekdays = $this->get_schedule_weekdays( $today, $now, $timezone );
			$weekdates = $this->get_schedule_weekdates( $weekdays, $now, $timezone );
		}

		$conflicts = $checked_shifts = array();
		if ( count( $all_shifts ) > 0 ) {
			$prev_shift = $prev_prev_shift = false;
			// foreach ( $all_shifts as $day => $shifts ) {
			foreach ( $weekdays as $day ) {

				if ( isset( $all_shifts[$day] ) ) {
					$shifts = $all_shifts[$day];

					// --- get previous and next days for comparisons ---
					// 2.3.2: fix to use week date schedule
					$thisdate = $weekdates[$day];
					$date_time = $this->to_time( $weekdates[$day] . ' 00:00', $timezone );

					// --- check for conflicts (overlaps) ---
					foreach ( $shifts as $key => $shift ) {

						// --- set first shift checked ---
						// 2.3.2: added for checking against last shift
						if ( !isset( $first_shift ) ) {
							$first_shift = $shift;
						}
						$last_shift = $shift;

						// --- reset shift switches ---
						$set_shift = true;
						$conflict = false;
						$disabled = ( isset( $shift['disabled'] ) && ( 'yes' == $shift['disabled'] ) ) ? true : false;

						// --- account for split midnight times ---
						// 2.3.2: replace strtotime with to_time for timezones
						// # midnight
						if ( ( '00:00 am' == $shift['start'] ) || ( '12:00 am' == $shift['start'] ) ) {
							$start_time = $this->to_time( $thisdate . ' 00:00', $timezone );
						} else {
							$shift_start = $this->convert_shift_time( $shift['start'] );
							$start_time = $this->to_time( $thisdate . ' ' . $shift_start, $timezone );
						}
						if ( ( '11:59:59 pm' == $shift['end'] ) || ( '12:00 am' == $shift['end'] ) ) {
							$end_time = $this->to_time( $thisdate . ' 11:59:59', $timezone ) + 1;
						} else {
							$shift_end = $this->convert_shift_time( $shift['end'] );
							$end_time = $this->to_time( $thisdate . ' ' . $shift_end, $timezone );
						}

						if ( false != $prev_shift ) {

							// note: previous shift start and end times set in previous loop iteration
							if ( $this->debug ) {
								$debug = "Shift Date: " . $thisdate . " - Day: " . $day . " - Time: " . $date_time . PHP_EOL;
								$prevdata = $prev_shift['shift'];
								$prevday = $prev_shift['day'];
								$prevdate = $prev_shift['date'];
								$debug .= "Previous Shift Date: " . $prevdate . " - Shift Day: " . $prevday . PHP_EOL;
								$debug .= "Shift: " . print_r( $shift, true ) . PHP_EOL;
								$debug .= "Previous Shift: " . print_r( $prev_shift, true ) . PHP_EOL;
								$debug .= "Weekdays: " . print_r( $weekdays, true ) . PHP_EOL;
								$debug .= "Weekdates: " . print_r( $weekdates, true ) . PHP_EOL;
								$this->debug_log( $debug );
							}

							// --- detect shift conflicts ---
							// (and maybe *attempt* to fix them up)
							if ( isset( $prev_start_time ) && ( $start_time == $prev_start_time ) ) {
								if ( $shift['split'] || $prev_shift['split'] ) {
									$conflict = 'overlap';
									if ( $shift['split'] && $prev_shift['split'] ) {
										// - need to compare start times on previous day -
										// 2.3.2: replace strtotime with to_time for timezones
										// 2.3.2: fix to convert to 24 hour format first
										$data = $shift['shift'];
										$real_start = $this->convert_shift_time( $data['real_start'] );
										$shiftdate = $this->get_previous_date( $thisdate );
										$real_start_time = $this->to_time( $shiftdate . ' ' . $real_start, $timezone );

										// 2.3.2: fix to calculation of previous shift day start time
										$prevdata = $prev_shift['shift'];
										// $prevday = $prevdata['day'];
										// $prevdate = %this->get_previous_date( $thisdate, $prevday );
										$prevdate = $prev_shift['date'];
										$prev_real_start = $this->convert_shift_time( $prevdata['real_start'] );
										$prev_real_start_time = $this->to_time( $prevdate . ' ' . $prev_real_start, $timezone );

										// --- compare start times ---
										if ( $real_start_time > $prev_real_start_time ) {
											// - current shift started later (overwrite from midnight) -
											$set_shift = true;
										} elseif ( $real_start_time == $prev_real_start_time ) {
											// - do not duplicate, already recorded -
											$conflict = false;
											// - total overlap, check last updated post time -
											$updated = strtotime( $shift['updated'] );
											$prev_updated = strtotime( $prev_shift['updated'] );
											if ( $updated < $prev_updated ) {
												$set_shift = false;
											}
										}
									} elseif ( $shift['split'] ) {
										// - the current shift has been split overnight -
										// assume previous shift is correct (ignore new shift time)
										$set_shift = false;
									} elseif ( $prev_shift['split'] ) {
										// the previous shift has been split overnight
										// so we will assume the new shift start is correct
										// (overwrites previous shift from midnight key)
										$set_shift = true;
									}
								} else {
									$conflict = 'same_start';
									// - we do not know which of these is correct -
									// no solution here, so check most recent last updated time
									// we will assume (without certainty) most recent is correct
									$updated = strtotime( $shift['updated'] );
									$prev_updated = strtotime( $prev_shift['updated'] );
									if ( $updated < $prev_updated ) {
										$set_shift = false;
									}
								}
							} elseif ( isset( $prev_end_time ) && ( $start_time < $prev_end_time ) ) {

								// 2.5.0: fix to missing variable operator on prev_end_time
								if ( ( $end_time > $prev_start_time ) || ( $end_time > $prev_end_time ) ) {

									// --- set the previous shift end time to current shift start ---
									$conflict = 'overlap';

									// --- modify only if this shift is not disabled ---
									if ( !$disabled ) {
										// 2.3.2: variable type fix (from checked_shift)
										// 2.3.2: fix for midnight starting aplit shifts
										// 2.3.2: set checked shifts with day key directly
										if ( '00:00 am' == $prev_shift['start'] ) {
											$prev_shift['start'] = '12:00 am';
										}
										$checked_shifts[$day][$prev_shift['start']]['end'] = $shift['start'];
										$checked_shifts[$day][$prev_shift['start']]['trimmed'] = true;

										if ( $this->debug ) {
											$debug = "Previous Previous Shift: " . print_r( $prev_prev_shift, true );
											$this->debug_log( $debug );
										}

										// --- fix for real end of first part of previous split shift ---
										if ( isset( $prev_shift['split'] ) && $prev_shift['split'] && isset( $prev_shift['real_start'] ) ) {
											if ( isset( $prev_prev_shift ) && isset( $prev_prev_shift['split'] ) && $prev_prev_shift['split'] ) {
												$checked_shifts[$prev_prev_shift['start']]['real_end'] = $shift['start'];
												$checked_shifts[$prev_prev_shift['start']]['trimmed'] = true;
											}
										}
									}

									// --- conflict debug output ---
									if ( $this->debug ) {
										$debug = "Conflicting Start Time: " . $this->get_time( "m-d l H:i", $start_time ) . " (" . $start_time . ")" . PHP_EOL;
										$debug .= '[ ' . $this->get_time( "m-d l H:i", $start_time ) . ' ]';
										$debug .= "Overlaps previous End Time: " .  $this->get_time( "m-d l H:i", $prev_end_time ) . " (" . $prev_end_time . ")" . PHP_EOL;
										$debug .= '[ ' . $this->get_time( "m-d l H:i", $prev_end_time ) . ' ]';
										// $debug .= "Shift: " . print_r( $shift, true );
										// $debug .= "Previous Shift: " . print_r( $prev_shift, true );
										$this->debug_log( $debug );
									}
								}
							}
						}

						// --- maybe store shift conflict data ---
						if ( $conflict ) {

							// ---- set short shift time data ---
							$shift_start = $shift['shift']['start_hour'] . ':' . $shift['shift']['start_min'] . $shift['shift']['start_meridian'];
							$shift_end = $shift['shift']['end_hour'] . ':' . $shift['shift']['end_min'] . $shift['shift']['end_meridian'];
							$prev_shift_start = $prev_shift['shift']['start_hour'] . ':' . $prev_shift['shift']['start_min'] . $prev_shift['shift']['start_meridian'];
							$prev_shift_end = $prev_shift['shift']['end_hour'] . ':' . $prev_shift['shift']['end_min'] . $prev_shift['shift']['end_meridian'];
							// 2.5.6: added for undefined index warning
							if ( !isset( $prev_disabled ) ) {
								$prev_disabled = '';
							}

							// --- store conflict for this shift ---
							// $conflicts[$shift['show']][] = array(
							$conflicts[$shift['ID']][] = array(
								// 'show'          => $shift['show'],
								'ID'            => $shift['ID'],
								'day'           => $shift['shift']['day'],
								'start'         => $shift_start,
								'end'           => $shift_end,
								'disabled'      => $disabled,
								// 'with_show'     => $prev_shift['show'],
								'with_show'     => $prev_shift['ID'],
								'with_day'      => $prev_shift['shift']['day'],
								'with_start'    => $prev_shift_start,
								'with_end'      => $prev_shift_end,
								'with_disabled' => $prev_disabled,
								'conflict'      => $conflict,
								'duplicate'     => false,
							);

							// --- store for previous shift only if a different show ---
							// if ( $shift['show'] != $prev_shift['show'] ) {
							if ( $shift['ID'] != $prev_shift['ID'] ) {
								$conflicts[$prev_shift['ID']][] = array(
									// 'show'          => $prev_shift['show'],
									'ID'            => $shift['ID'],
									'day'           => $prev_shift['shift']['day'],
									'start'         => $prev_shift_start,
									'end'           => $prev_shift_end,
									'disabled'      => $prev_disabled,
									// 'with_show'     => $shift['show'],
									'with_show'     => $shift['ID'],
									'with_day'      => $shift['shift']['day'],
									'with_start'    => $shift_start,
									'with_end'      => $shift_end,
									'with_disabled' => $disabled,
									'conflict'      => $conflict,
									'duplicate'     => true,
								);
							}
						}

						// --- set current shift to previous for next iteration ---
						$prev_start_time = $start_time;
						$prev_end_time = $end_time;
						if ( $prev_shift ) {
							$prev_prev_shift = $prev_shift;
						}
						$prev_shift = $shift;
						$prev_disabled = $disabled;

						// --- set the now checked shift data ---
						// (...but only if not disabled!)
						if ( $set_shift && !$disabled ) {
							// - no longer need shift and post updated times -
							// 2.3.3.9: keep shift ID with schedule data
							$shift['id'] = $shift['shift']['id'];
							unset( $shift['shift'] );
							unset( $shift['updated'] );
							if ( '00:00 am' == $shift['start'] ) {
								$shift['start'] = '12:00 am';
							}
							// 2.3.2: set checked shifts with day key directly
							$checked_shifts[$day][$shift['start']] = $shift;
						}
					}
				}

				// --- set checked shifts for day ---
				// 2.3.2: set checked shifts with day key directly
				// $all_shifts[$day] = $checked_shifts;
			}
		}

		// --- check last shift against first shift ---
		// 2.3.2: added for possible overlap (split by weekly schedule dates)
		// 2.5.6: added isset check for first_shift
		if ( isset( $last_shift ) && isset( $first_shift ) && ( $last_shift != $first_shift ) ) {

			// --- use days for different weeks to compare ---
			$l_shift_start = $this->convert_shift_time( $last_shift['start'] );
			$l_shift_end = $this->convert_shift_time( $last_shift['end'] );
			$last_shift_start = $this->to_time( $last_shift['day'] . ' ' . $l_shift_start, $timezone );
			$last_shift_end = $this->to_time( $last_shift['day'] . ' ' . $l_shift_end, $timezone );
			if ( $last_shift_end < $last_shift_start ) {
				$last_shift_end = $last_shift_end + ( 24 * 60 * 60 );
			}

			$f_shift_start = $this->convert_shift_time( $first_shift['start'] );
			$f_shift_end = $this->convert_shift_time( $first_shift['end'] );
			$first_shift_start = $this->to_time( 'next ' . $first_shift['day'] . ' ' . $f_shift_start, $timezone );
			$first_shift_end = $this->to_time( 'next ' . $first_shift['day'] . ' ' . $f_shift_end, $timezone );
			if ( $first_shift_end < $first_shift_start ) {
				$first_shift_end = $first_shift_end + ( 24 * 60 * 60 );
			}

			if ( $this->debug ) {
				$debug = 'Last Shift End: ' . $last_shift['day'] . ' ' . $l_shift_end . ' - (' . $last_shift_end . ')' . PHP_EOL;
				$debug .=  'First Shift Start: ' . $first_shift['day'] . ' ' . $f_shift_start . ' - (' . $first_shift_start . ')' . PHP_EOL;
				$this->debug_log( $debug );
			}

			// --- end of the week overlap check ---
			// 2.3.3.9: fix to incorrect overlap logic
			if ( $last_shift_end > $first_shift_start ) {
			// if ( $last_shift_start < $first_shift_end ) {

				// --- record a conflict ---
				if ( $this->debug ) {
					$debug = "First/Last Shift Overlap Conflict" . PHP_EOL;
					$debug .= "First Shift: " . print_r( $first_shift, true ) . PHP_EOL;
					$debug .= "Last Shift: " . print_r( $last_shift, true ) . PHP_EOL;
					$this->debug_log( $debug );
				}

				/*
				// --- store conflict for this shift ---
				$conflicts[$first_shift['show']][] = array(
					'show'          => $first_shift['show'],
					'day'           => $first_shift['shift']['day'],
					'start'         => $first_shift_start,
					'end'           => $first_shift_end,
					'disabled'      => $first_shift['shift']['disabled'],
					'with_show'     => $last_shift['show'],
					'with_day'      => $last_shift['shift']['day'],
					'with_start'    => $last_shift_start,
					'with_end'      => $last_shift_end,
					'with_disabled' => $last_shift['shift']['disabled'],
					'conflict'      => 'overlap',
					'duplicate'     => false,
				);

				// --- store for other shift if different show ---
				if ( $first_shift['show'] != $last_shift['show'] ) {
					$conflicts[$last_shift['show']][] = array(
						'show'          => $last_shift['show'],
						'day'           => $last_shift['shift']['day'],
						'start'         => $last_shift_start,
						'end'           => $last_shift_end,
						'disabled'      => $last_shift['shift']['disabled'],
						'with_show'     => $first_shift['show'],
						'with_day'      => $first_shift['shift']['day'],
						'with_start'    => $first_shift_start,
						'with_end'      => $first_shift_end,
						'with_disabled' => $first_shift['shift']['disabled'],
						'conflict'      => 'overlap',
						'duplicate'     => true,
					);
				} */
			}
		}

		// --- do shift conflict action ---
		do_action( 'schedule_engine_set_shift_conflicts', $conflicts, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			do_action( $context . '_set_shift_conflicts', $conflicts, $channel );
		}
		
		// --- return checked results ---
		return $checked_shifts;
	}

	// -------------
	// Shift Checker
	// -------------
	// (checks shift being saved against other shows)
	public function check_shift( $record_id, $shift, $scope, $all_shifts, $times = false ) {

		// --- set channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// 2.3.2: bug out if day is empty
		if ( '' == $shift['day'] ) {
			return false;
		}

		// --- convert days to dates for checking ---
		// 2.3.3.9: fix weekday/dates code to match get_show_shifts
		if ( $times ) {
			$now = $times['now'];
			$timezone = $times['timezone'];
			$today = $times['today'];
			$weekdays = $times['weekdays'];
			$weekdates = $times['weekdates'];
		} else {
			$now = $this->get_now();
			$timezone = $this->get_timezone();
			$today = $this->get_time( 'l', $now, $timezone );
			$weekdays = $this->get_schedule_weekdays( $today, $now, $timezone );
			$weekdates = $this->get_schedule_weekdates( $weekdays, $now, $timezone );
		}

		// --- get shows to check against via scope ---
		$check_shifts = array();
		if ( 'all' == $scope ) {
			$check_shifts = $all_shifts;
		} elseif ( 'other' == $scope ) {
			// --- check only against other shifts ---
			// 2.5.0: changed scope from shows to other
			foreach ( $all_shifts as $day => $day_shifts ) {
				foreach ( $day_shifts as $start => $day_shift ) {
					// --- ...so remove (skip) any shifts for this record ---
					// 2.5.0: changed show key to ID key
					// if ( $day_shift['show'] != $record_id ) {
					if ( $day_shift['ID'] != $record_id ) {
						$check_shifts[$day][$start] = $day_shift;
					}
				}
			}
		}

		// 2.3.2: to doubly ensure shifts are set in schedule order
		$sorted_shifts = array();
		foreach ( $weekdays as $weekday ) {
			if ( isset( $check_shifts[$weekday] ) ) {
				$sorted_shifts[$weekday] = $check_shifts[$weekday];
			}
		}
		$check_shifts = $sorted_shifts;

		// --- get shift start and end time ---
		// 2.3.2: fix to convert to 24 hour times first
		// 2.3.3.9: set shift start and end to prevent undefined index warning later
		$shift['start'] = $shift['start_hour'] . ':' . $shift['start_min'] . $shift['start_meridian'];
		$shift['end'] = $shift['end_hour'] . ':' . $shift['end_min'] . $shift['end_meridian'];
		$start_time = $this->convert_shift_time( $shift['start'] );
		$end_time = $this->convert_shift_time( $shift['end'] );

		// 2.3.2: use next week day instead of date
		$shift_start_time = $this->to_time( $weekdates[$shift['day']] . ' ' . $start_time, $timezone );
		$shift_end_time = $this->to_time( $weekdates[$shift['day']] . ' ' . $end_time, $timezone );
		// 2.3.3.9: added or equals to operator
		if ( $shift_end_time <= $shift_start_time ) {
			$shift_end_time = $shift_end_time + ( 24 * 60 * 60 );
		}

		if ( $this->debug ) {
			$debug = "Checking Shift for Show " . $record_id . ": ";
			$debug .= $shift['day'] . " - " . $weekdates[$shift['day']] . " - " . $shift['start_hour'] . ":" . $shift['start_min'] . $shift['start_meridian'];
			$debug .= " (" . $shift_start_time . ")" . PHP_EOL;
			$debug .= " to " . $weekdates[$shift['day']] . " - " . $shift['end_hour'] . ":" . $shift['end_min'] . $shift['end_meridian'];
			$debug .= " (" . $shift_end_time . ")" . PHP_EOL;
			$this->debug_log( $debug );
		}

		// --- check for conflicts with other show shifts ---
		$conflicts = array();
		foreach ( $check_shifts as $day => $day_shifts ) {
			// 2.3.2: removed day match check
			// if ( $day == $shift['day'] ) {
				foreach ( $day_shifts as $i => $day_shift ) {

					if ( !isset( $first_shift ) ) {
						$first_shift = $day_shift;
					}

					// 2.3.2: replace strtotime with to_time for timezones
					// 2.3.2: fix to convert to 24 hour times first
					$day_shift_start = $this->convert_shift_time( $day_shift['start'] );
					$day_shift_end = $this->convert_shift_time( $day_shift['end'] );

					// 2.3.2: use next week day instead of date
					$day_shift_start_time = $this->to_time( $day_shift['date'] . ' ' . $day_shift_start, $timezone );
					$day_shift_end_time = $this->to_time( $day_shift['date'] . ' ' . $day_shift_end, $timezone );
					// 2.3.2: adjust for midnight with change to use non-split shifts
					// 2.3.3.9: added or equals to operator
					if ( $day_shift_end_time <= $day_shift_start_time ) {
						$day_shift_end_time = $day_shift_end_time + ( 24 * 60 * 60 );
					}

					// --- ignore if this is the same shift we are checking ---
					$check_shift = true;
					// if ( $day_shift['show'] == $record_id ) {
					if ( $day_shift['ID'] == $record_id ) {
						// ? only ignore same shift not same show ?
						// if ( ( $day_shift_start_time == $shift_start_time ) && ( $day_shift_end_time == $shift_end_time ) ) {
							$check_shift = false;
						// }
					}

					if ( $check_shift ) {

						if ( $this->debug ) {
							$debug = "...with Shift for Show " . $day_shift['ID'] . ": ";
							$debug .= $day_shift['day'] . " - " . $day_shift['date'] . " - " . $day_shift['start'] . " (" . $day_shift_start_time . ")" . PHP_EOL;
							$debug .= " to " . $day_shift['end'] . " (" . $day_shift_end_time . ")" . PHP_EOL;
							$this->debug_log( $debug );
						}

						// 2.3.2: improved shift checking logic
						// 2.3.3.6: separated logic for conflict match code
						$conflict = false;
						if ( ( $shift_start_time < $day_shift_start_time ) && ( $shift_end_time > $day_shift_start_time ) ) {
							// if the new shift starts before existing shift but ends after existing shift starts
							$conflict = 'start overlap';
						} elseif ( ( $shift_start_time < $day_shift_start_time ) && ( $shift_end_time > $day_shift_end_time ) ) {
							// ...or starts before but ends after the existing shift end time
							$conflict = 'blockout overlap';
						} elseif ( ( $shift_start_time == $day_shift_start_time ) ) {
							// ...or new shift starts at the same time as the existing shift
							$conflict = 'equal start time';
						} elseif ( ( $shift_start_time > $day_shift_start_time ) && ( $shift_end_time < $day_shift_end_time ) ) {
							// ...or if the new shift starts after existing shift and ends before it ends
							$conflict = 'internal overlap';
						} elseif ( ( $shift_start_time > $day_shift_start_time ) && ( $shift_start_time < $day_shift_end_time ) ) {
							// ...or the new shift starts after the existing shift but before it ends
							$conflict = 'end overlap';
						}
						if ( $conflict ) {
							// --- if there is a shift overlap conflict ---
							$conflicts[] = $day_shift;
							if ( $this->debug ) {
								$debug = '^^^ CONFLICT ( ' . $conflict . ' ) ^^^';
								$this->debug_log( $debug );
							}
						}
					}
				}
			// }
		}

		// --- recheck for first shift overlaps ---
		// (not implemented as not needed)
		/* if ( isset( $first_shift ) ) {
			// --- check for first shift overlap using next week ---
			$shift_start = $this->convert_shift_time( $first_shift['start'] );
			$shift_end = $this->convert_shift_time( $first_shift['end'] );
			$first_shift_start_time = $this->to_time( $first_shift['date'] . ' ' . $shift_start, $timezone ) + ( 7 * 24 * 60 * 60 );
			$first_shift_end_time = $this->to_time( $first_shift['date'] . ' ' . $shift_end, $timezone ) + ( 7 * 24 * 60 * 60 );

			if ( $this->debug ) {
				echo "...with First Shift for Show " . $first_shift['show'] . ": ";
				echo $first_shift['day'] . " - " . $first_shift['date'] . " - " . $first_shift['start'] . " (" . $first_shift_start_time . ")";
				echo " to " . $first_shift['end'] . " (" . $first_shift_end_time . ")" . PHP_EOL;
			}

			if ( ( ( $shift_start_time < $first_shift_start_time ) && ( $shift_end_time > $first_shift_start_time ) )
				 || ( ( $shift_start_time < $first_shift_start_time ) && ( $shift_end_time > $first_shift_end_time ) )
				 || ( $shift_start_time == $first_shift_start_time )
				 || ( ( $shift_start_time > $first_shift_start_time ) && ( $shift_end_time < $first_shift_end_time ) )
				 || ( ( $shift_start_time > $first_shift_start_time ) && ( $shift_start_time < $first_shift_end_time ) ) ) {
				$conflicts[] = $first_shift;
				if ( $this->debug ) {
					echo "^^^ CONFLICT ^^^" . PHP_EOL;
				}
			}
		} */

		// --- recheck for last shift ---
		// (for date based schedule overflow rechecking)
		if ( isset( $day_shift ) ) {

			// 2.3.3.6: added check to not last check shift against itself
			// 2.5.1: check all indexes exist to avoid undefined index warnings
			// 2.5.6: fix to check ID key instead of show key
			// if ( ( isset( $day_shift['show'] ) && ( $day_shift['show'] != $record_id ) )
			if ( ( isset( $day_shift['ID'] ) && ( $day_shift['ID'] != $record_id ) )
				|| ( isset( $day_shift['day'] ) && ( $day_shift['day'] != $shift['day'] ) )
				|| ( isset( $day_shift['start'] ) && ( $day_shift['start'] != $shift['start'] ) )
				|| ( isset( $day_shift['end'] ) && ( $day_shift['end'] != $shift['end'] ) ) ) {

				// --- check for new shift overlap using next week ---
				$shift_start_time = $shift_start_time + ( 7 * 24 * 60 * 60 );
				$shift_end_time = $shift_end_time + ( 7 * 24 * 60 * 60 );

				if ( $this->debug ) {
					$debug = "...with Last Shift (using next week):" . PHP_EOL;
					// echo $this->get_time( 'date', $shift_start_time ) . " - " . $shift['start'] . " (" . $shift_start_time . ")";
					// echo " to " . $shift['end'] . " (" . $shift_end_time . ")" . PHP_EOL;
					$debug .= $day_shift['day'] . " - " . $this->get_time( 'date', $day_shift_start_time );
					$debug .= " - " . $day_shift['start'] . " (" . $day_shift_start_time . ")";
					$debug .= " to " . $day_shift['end'] . " (" . $day_shift_end_time . ")" . PHP_EOL;
					$this->debug_log( $debug );
				}

				// 2.3.3.6: separated logic for conflict match code
				$conflict = false;
				if ( ( $shift_start_time < $day_shift_start_time ) && ( $shift_end_time > $day_shift_start_time ) ) {
					$conflict = 'start overlap';
				} elseif ( ( $shift_start_time < $day_shift_start_time ) && ( $shift_end_time > $day_shift_end_time ) ) {
					$conflict = 'blockout overlap';
				} elseif ( $shift_start_time == $day_shift_start_time ) {
					$conflict = 'equal start time';
				} elseif ( ( $shift_start_time > $day_shift_start_time ) && ( $shift_end_time < $day_shift_end_time ) ) {
					$conflict = 'internal overlap';
				} elseif ( ( $shift_start_time > $day_shift_start_time ) && ( $shift_start_time < $day_shift_end_time ) ) {
					$conflict = 'end overlap';
				}
				if ( $conflict ) {
					$conflicts[] = $day_shift;
					if ( $this->debug ) {
						$debug = '^^^ CONFLICT ( ' . $conflict . ') ^^^';
						$this->debug_log( $debug );
					}
				}
			}
		}

		if ( count( $conflicts ) == 0 ) {
			return false;
		}

		return $conflicts;
	}

	// ------------------
	// New Shifts Checker
	// ------------------
	// (checks show shifts for conflicts with same show)
	public function check_new_shifts( $new_shifts, $weekdates = false ) {
		
		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- debug point ---
		if ( $this->debug ) {
			$debug = "New Shifts: " . print_r( $new_shifts, true ) . PHP_EOL;
			$this->debug_log( $debug );
		}

		// --- convert days to dates for checking ---
		$timezone = $this->get_timezone();
		if ( !$weekdates ) {
			$now = $this->get_now();
			$today = $this->get_time( 'l', $now, $timezone );
			$weekdays = $this->get_schedule_weekdays( $today, $now, $timezone );
			$weekdates = $this->get_schedule_weekdates( $weekdays, $now, $timezone );
		}

		// --- double loop shifts to check against others ---
		$new_shifts_in = $new_shifts;
		foreach ( $new_shifts as $i => $shift_a ) {

			if ( '' != $shift_a['day'] ) {

				// --- get shift A start and end times ---
				// 2.3.2: replace strtotime with to_time for timezones
				// 2.3.2: fix to convert to 24 hour format first
				$shift_a_start = $shift_a['start_hour'] . ':' . $shift_a['start_min'] . $shift_a['start_meridian'];
				$shift_a_end = $shift_a['end_hour'] . ':' . $shift_a['end_min'] . $shift_a['end_meridian'];
				$shift_a_start = $this->convert_shift_time( $shift_a_start );
				$shift_a_end = $this->convert_shift_time( $shift_a_end );

				// 2.3.2: use next week day instead of date
				$shift_a_start_time = $this->to_time( $weekdates[$shift_a['day']] . ' ' . $shift_a_start );
				$shift_a_end_time = $this->to_time( $weekdates[$shift_a['day']] . ' ' . $shift_a_end );
				// $shift_a_start_time = $this->to_time( '+2 weeks ' . $shift_a['day'] . ' ' . $shift_a_start );
				// $shift_a_end_time = $this->to_time( '+2 weeks ' . $shift_a['day'] . ' ' . $shift_a_end );
				// 2.3.3.9: added or equals to operator
				if ( $shift_a_end_time <= $shift_a_start_time ) {
					$shift_a_end_time = $shift_a_end_time + ( 24 * 60 * 60 );
				}

				// --- debug point ---
				if ( $this->debug ) {
					$a_start = $shift_a['day'] . ' ' . $shift_a['start_hour'] . ':' . $shift_a['start_min'] . $shift_a['start_meridian'] . ' (' . $shift_a_start_time . ')';
					$a_end = $shift_a['day'] . ' ' . $shift_a['end_hour'] . ':' . $shift_a['end_min'] . $shift_a['end_meridian'] . ' (' . $shift_a_end_time . ')';
					$debug = "Shift A Start: " . $a_start . ' - Shift A End: ' . $a_end . "\n";
					$this->debug_log( $debug );
				}

				$debug = '';
				foreach ( $new_shifts as $j => $shift_b ) {

					if ( $i != $j ) {

						if ( $this->debug ) {
							$debug .= $i . ' ::: ' . $j . "\n";
						}

						if ( '' != $shift_b['day'] ) {
							// --- get shift B start and end times ---
							// 2.3.2: replace strtotime with to_time for timezones
							$shift_b_start = $shift_b['start_hour'] . ':' . $shift_b['start_min'] . $shift_b['start_meridian'];
							$shift_b_end = $shift_b['end_hour'] . ':' . $shift_b['end_min'] . $shift_b['end_meridian'];
							$shift_b_start = $this->convert_shift_time( $shift_b_start );
							$shift_b_end = $this->convert_shift_time( $shift_b_end );

							// 2.3.2: use next week day instead of date
							$shift_b_start_time = $this->to_time( $weekdates[$shift_b['day']] . ' ' . $shift_b_start, $timezone );
							$shift_b_end_time = $this->to_time( $weekdates[$shift_b['day']] . ' ' . $shift_b_end, $timezone );
							// 2.3.3.9: added or equals to operator
							if ( $shift_b_end_time <= $shift_b_start_time ) {
								$shift_b_end_time = $shift_b_end_time + ( 24 * 60 * 60 );
							}

							// --- debug point ---
							if ( $this->debug ) {
								$b_start = $shift_b['day'] . ' ' . $shift_b_start . ' (' . $shift_b_start_time . ')';
								$b_end = $shift_b['day'] . ' ' . $shift_b_end . ' (' . $shift_b_end_time . ')';
								$debug .= "with Shift B Start: " . $b_start . ' - Shift B End: ' . $b_end . PHP_EOL;
							}

							// --- compare shift A and B times ---
							if ( ( ( $shift_a_start_time < $shift_b_start_time ) && ( $shift_a_end_time > $shift_b_start_time ) )
									|| ( ( $shift_a_start_time < $shift_b_start_time ) && ( $shift_a_end_time > $shift_b_end_time ) )
									|| ( $shift_a_start_time == $shift_b_start_time )
									|| ( ( $shift_a_start_time > $shift_b_start_time ) && ( $shift_a_end_time < $shift_b_end_time ) )
									|| ( ( $shift_a_start_time > $shift_b_start_time ) && ( $shift_a_start_time < $shift_b_end_time ) ) ) {

								// --- maybe disable shift B ---
								// 2.3.2: added check for isset on disabled key
								if ( ( !isset( $new_shifts[$i]['disabled'] ) || ( 'yes' != $new_shifts[$i]['disabled'] ) )
									&& ( !isset( $new_shifts[$j]['disabled'] ) || ( 'yes' != $new_shifts[$j]['disabled'] ) ) ) {

									// --- debug point ---
									if ( $this->debug ) {
										// TODO: write more detailed explanations for debug conditions
										$debug .= "* Conflict Found! New Shift (B) Disabled ";
										if ( ( $shift_a_start_time < $shift_b_start_time ) && ( $shift_a_end_time > $shift_b_start_time ) ) {
											$debug .= "[A]";
										}
										if ( ( $shift_a_start_time < $shift_b_start_time ) && ( $shift_a_end_time > $shift_b_end_time ) ) {
											$debug .= "[B]";
										}
										if ( $shift_a_start_time == $shift_b_start_time ) {
											$debug .= "[C]";
										}
										if ( ( $shift_a_start_time > $shift_b_start_time ) && ( $shift_a_end_time < $shift_b_end_time ) ) {
											$debug .= "[D]";
										}
										if ( ( $shift_a_start_time > $shift_b_start_time ) && ( $shift_a_start_time < $shift_b_end_time ) ) {
											$debug .= "[E]";
										}
										$debug .= "*" . PHP_EOL;
									}

									$new_shifts[$j]['disabled'] = 'yes';

								} else {
									if ( $this->debug ) {
										$debug .= "[Conflict with disabled shift.]" . PHP_EOL;
									}
								}
							}
						}
					}
				}
			}
		}

		// --- debug point ---
		if ( $this->debug ) {
			$debug .= "Checked New Shifts: " . print_r( $new_shifts, true ) . PHP_EOL;
			$this->debug_log( $debug, false );
		}

		// --- filter and return ---
		$new_shifts = apply_filters( 'schedule_engine_checked_new_shifts', $new_shifts, $new_shifts_in, $weekdates, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$new_shifts = apply_filters( $context . '_checked_new_shifts', $new_shifts, $new_shifts_in, $weekdates, $channel );
		}
		return $new_shifts;
	}

	// -------------------
	// Validate Shift Time
	// -------------------
	// 2.3.0: added check for incomplete shift times
	public function validate_shift( $shift ) {

		if ( '' == $shift['day'] ) {
			$shift['disabled'] = 'yes';
		}
		if ( ( '' == $shift['start_meridian'] ) || ( '' == $shift['end_meridian'] ) ) {
			$shift['disabled'] = 'yes';
		}
		if ( ( '' == $shift['start_hour'] ) || ( '' == $shift['end_hour'] ) ) {
			$shift['disabled'] = 'yes';
		}
		if ( '' == $shift['start_min'] ) {
			$shift['start_min'] = '00';
		}
		if ( '' == $shift['end_min'] ) {
			$shift['end_min'] = '00';
		}

		return $shift;
	}


	// ------------------------
	// === Time Conversions ===
	// ------------------------


	// -------
	// Get Now
	// -------
	public function get_now( $gmt = true ) {

		$channel = $this->channel;
		$context = $this->context;

		if ( defined( 'SCHEDULE_ENGINE_USE_SERVER_TIMES' ) && SCHEDULE_ENGINE_USE_SERVER_TIMES ) {
			$now = strtotime( current_time( 'mysql' ) );
		} else {
			$datetime = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
			$now = $datetime->format( 'U' );
		}
		
		// 2.5.6: allow explicit override of now time
		if ( isset( $_REQUEST['for_time'] ) ) {
			$now = absint( $_REQUEST['for_time'] );
		}

		// 2.5.6: added programmatic filter for now time
		$now = apply_filters( 'schedule_engine_now_time', $now, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$now = apply_filters( $context . '_now_time', $now, $channel );
		}
		return $now;
	}

	// ------------
	// Get Timezone
	// ------------
	public function get_timezone() {
		
		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- fallback to WordPress timezone ---
		$timezone = get_option( 'timezone_string' );
		if ( false !== strpos( $timezone, 'Etc/GMT' ) ) {
			$offset = get_option( 'gmt_offset' );
			$timezone = 'UTC' . $offset;
		} elseif ( '' == $timezone ) {
			// 2.5.0: handle empty (not set) timezone location
			$timezone = 'UTC';
		}

		$timezone = apply_filters( 'schedule_engine_timezone', $timezone, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$timezone = apply_filters( $context . '_timezone', $timezone, $channel );
		}
		return $timezone;
	}

	// -----------------
	// Get Timezone Code
	// -----------------
	// note: this should only be used for display purposes
	// (as the actual code used is based on timezone/location)
	public function get_timezone_code( $timezone ) {
		$datetime = new DateTime( 'now', new DateTimeZone( $timezone ) );
		$code = $datetime->format( 'T' );
		return $code;
	}

	// --------------------
	// Get Date Time Object
	// --------------------
	// 2.3.2: added for consistent timezone conversions
	public function get_date_time( $timestring, $timezone ) {

		// echo "*TIMEZONE*: " . $timezone . PHP_EOL;

		if ( 'UTC' == $timezone ) {
			$utc = new DateTimeZone( 'UTC' );
			$datetime = new DateTime( $timestring, $utc );
		} elseif ( strstr( $timezone, 'UTC' ) ) {
			$offset = str_replace( 'UTC', '', $timezone );
			$offset = (int) $offset * 60 * 60;
			$utc = new DateTimeZone( 'UTC' );
			$datetime = new DateTime( $timestring, $utc );
			$timestamp = (int) $datetime->format( 'U' );
			$timestamp = $timestamp + $offset;
			$datetime->setTimestamp( $timestamp );
		} elseif ( $timezone ) {
			$datetime = new DateTime( $timestring, new DateTimeZone( $timezone ) );
			// ...fix to set timestamp again just in case
			// echo "A: " . print_r( $datetime, true ) . PHP_EOL;
			// if ( '@' == substr( $timestring, 0, 1 ) ) {
			//	$timestamp = substr( $timestring, 1, strlen( $timestring ) );
			//	$datetime->setTimestamp( $timestamp );
			// }
			// echo "B: " . print_r( $datetime, true ) . PHP_EOL;
		} else {
			// 2.5.6: debug output instead of exiting
			if ( $this->debug ) {
				$debug = 'Error in Time String Timezone: ' . $timestring . ' - Timezone: ' . $timezone;
				$this->debug_log( $debug );
			}
			return false;
		}

		return $datetime;
	}

	// --------------
	// String To Time
	// --------------
	// 2.3.2: added for timezone handling
	public function to_time( $timestring, $timezone = false ) {

		if ( defined( 'SCHEDULE_ENGINE_USE_SERVER_TIMES' ) && SCHEDULE_ENGINE_USE_SERVER_TIMES ) {
			$time = strtotime( $timestring );
		} else {
			if ( strstr( $timezone, 'UTC' ) && ( 'UTC' != $timezone ) ) {
				// --- fallback for UTC offsets ---
				$offset = str_replace( 'UTC', '', $timezone );
				$offset = (int) $offset * 60 * 60;
				$utc = new DateTimeZone( 'UTC' );
				$datetime = new DateTime( $timestring, $utc );
				$timestamp = $datetime->getTimestamp();
				$timestamp = $timestamp - $offset;
				$datetime->setTimestamp( $timestamp );
			} else {
				if ( !$timezone ) {
					$timezone = $this->get_timezone();
				}
				$datetime = $this->get_date_time( $timestring, $timezone );
			}
			$time = $datetime->format( 'U' );
		}

		return $time;
	}

	// ---------
	// Get Times
	// ---------
	// 2.5.0: added for separation / abstraction
	public function get_times( $time = false, $timezone = false ) {

		// --- get offset time and date ---
		if ( !$timezone || ( defined( 'SCHEDULE_ENGINE_USE_SERVER_TIMES' ) && SCHEDULE_ENGINE_USE_SERVER_TIMES ) ) {

			if ( !$time ) {
				$time = $this->get_now();
			}
			// 2.4.5: use gmdate here
			$day = gmdate( 'l', $time );
			$date = gmdate( 'Y-m-d', $time );
			$date_time = gmdate( 'Y-m-d H:i:s', $time );
			$timestamp = gmdate( 'U', $time );

		} else {

			// 2.5.0: shortened et timestring condition
			$timestring = $time ? '@' . $time : 'now';

			// --- get datetime object ---
			if ( strstr( $timezone, 'UTC' ) ) {
				$datetime = $this->get_date_time( $timestring, $timezone );
			} else {
				// ...and refix for location timezones
				$datetime = new DateTime( $timestring, new DateTimeZone( 'UTC' ) );
				$datetime->setTimezone( new DateTimeZone( $timezone ) );
			}

			// --- set formatted strings ---
			$day = $datetime->format( 'l' );
			$date = $datetime->format( 'Y-m-d' );
			$date_time = $datetime->format( 'Y-m-d H:i:s' );
			$timestamp = $datetime->format( 'U' );

		}

		// --- set times array ---
		$times = array(
			'day'       => $day,
			'date'      => $date,
			'datetime'  => $date_time,
			'timestamp' => $timestamp,
		);

		// --- maybe set datetime object ---
		if ( isset( $datetime ) ) {
			$times['object'] = $datetime;
		}

		return $times;
	}

	// --------
	// Get Time
	// --------
	// 2.3.2: added for timezone adjustments
	public function get_time( $key = false, $time = false, $timezone = false ) {

		$times = $this->get_times( $time, $timezone );

		if ( !$key ) {
			return $times;
		}

		if ( array_key_exists( $key, $times ) ) {
			// --- use preformatted time ---
			$time = $times[$key];
		} elseif ( isset( $times['object'] ) ) {
			// --- use datetime object for formatting ---
			$datetime = $times['object'];
			$time = $datetime->format( $key );
		} else {
			// --- fallback to server date ---
			$time = date( $key, $time );
		}
		return $time;
	}

	// --------------------
	// Get Timezone Options
	// --------------------
	// ref: (based on) https://stackoverflow.com/a/17355238/5240159
	public function get_timezone_options( $include_wp_timezone = false ) {

		// --- maybe get stored timezone options ---
		$options = get_transient( 'schedule_engine_timezone_options' );
		if ( !$options ) {

			// --- set regions ---
			$regions = array(
				DateTimeZone::AFRICA     => __( 'Africa', 'schedule-engine' ),
				DateTimeZone::AMERICA    => __( 'America', 'schedule-engine' ),
				DateTimeZone::ASIA       => __( 'Asia', 'schedule-engine' ),
				DateTimeZone::ATLANTIC   => __( 'Atlantic', 'schedule-engine' ),
				DateTimeZone::AUSTRALIA  => __( 'Australia', 'schedule-engine' ),
				DateTimeZone::EUROPE     => __( 'Europe', 'schedule-engine' ),
				DateTimeZone::INDIAN     => __( 'Indian', 'schedule-engine' ),
				DateTimeZone::PACIFIC    => __( 'Pacific', 'schedule-engine' ),
				DateTimeZone::ANTARCTICA => __( 'Antarctica', 'schedule-engine' ),
			);

			// --- loop regions ---
			foreach ( $regions as $region => $label ) {

				// --- option group by region ---
				$options['*OPTGROUP*' . $region] = $label;

				$timezones = DateTimeZone::listIdentifiers( $region );
				$timezone_offsets = array();
				foreach ( $timezones as $timezone ) {
					$datetimezone = new DateTimeZone( $timezone );
					$offset = $datetimezone->getOffset( new DateTime() );
					$timezone_offsets[$offset][] = $timezone;
				}
				ksort( $timezone_offsets );

				foreach ( $timezone_offsets as $offset => $timezones ) {
					foreach ( $timezones as $timezone ) {
						$prefix = $offset < 0 ? '-' : '+';
						$hour = gmdate( 'H', abs( $offset ) );
						$minutes = gmdate( 'i', abs( $offset ) );
						$code = $this->get_timezone_code( $timezone );
						$label = $code . ' (GMT' . $prefix . $hour . ':' . $minutes . ') - ';
						$timezone_split = explode( '/', $timezone );
						unset( $timezone_split[0] );
						$timezone_joined = implode( '/', $timezone_split );
						$label .= str_replace( '_', ' ', $timezone_joined );
						$options[$timezone] = $label;
					}
				}
			}
			$expiry = 7 * 24 * 60 * 60;
			set_transient( 'schedule_engine_timezone_options', $options, $expiry );
		}

		// --- maybe add WordPress timezone (default) option ---
		if ( $include_wp_timezone ) {
			$wp_timezone = array( '' => __( 'WordPress Timezone', 'schedule-engine' ) );
			$options = array_merge( $wp_timezone, $options );
		}

		// --- filter and return ---
		$options = apply_filters( 'schedule_engine_timezone_options', $options, $include_wp_timezone );
		return $options;
	}

	// --------------
	// Get Weekday(s)
	// --------------
	// 2.3.2: added get weekday from number helper
	public function get_weekday( $day_number = null ) {

		// 2.5.0: use internal get_weekdays function
		$weekdays = $this->get_weekdays();
		if ( !is_null( $day_number ) ) {
			return $weekdays[$day_number];
		}
		return $weekdays;
	}

	// ------------
	// Get Month(s)
	// ------------
	// 2.3.2: added get weekday from number helper
	public function get_month( $month_number = null ) {

		// 2.5.0: use internal get_months function
		$months = $this->get_months();
		if ( !is_null( $month_number ) ) {
			return $months[$month_number];
		}
		return $months;
	}

	// ---------------------
	// Get Schedule Weekdays
	// ---------------------
	// note: no translations here because used internally for sorting
	// 2.3.0: added to get schedule weekdays from start of week
	// 2.5.0: added time and timezone arguments for when weekstart is today
	public function get_schedule_weekdays( $weekstart = false, $time = false, $timezone = false ) {
		
		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- maybe get start of the week ---
		if ( !$weekstart ) {
			$weekstart = get_option( 'start_of_week' );
			$weekstart = apply_filters( 'schedule_engine_schedule_weekday_start', $weekstart, $channel, $context );
			if ( ( '' != $context ) && is_string( $context ) ) {
				$weekstart = apply_filters( $context . '_schedule_weekday_start', $weekstart, $channel );
			}
		}

		// 2.5.0: use internal get_weekdays function
		$weekdays = $this->get_weekdays();

		// 2.3.2: also accept string format for weekstart
		if ( is_string( $weekstart ) ) {
			// 2.3.3.5: accept today as valid week start
			if ( 'today' == $weekstart ) {
				// 2.5.0: added time and timezone arguments
				$timezone = $timezone ? $timezone : $this->get_timezone();
				$weekstart = $this->get_time( 'day', $time, $timezone );
			}
			foreach ( $weekdays as $i => $weekday ) {
				if ( strtolower( $weekday ) == strtolower( $weekstart ) ) {
					$weekstart = $i;
				}
			}
		}

		// --- loop weekdays and reorder from start day ---
		$start = $before = $after = array();
		foreach ( $weekdays as $i => $weekday ) {
			// 2.3.2: allow matching of numerical index or weekday name
			if ( ( $i == $weekstart ) || ( $weekday == $weekstart ) ) {
				$start[] = $weekday;
			} elseif ( $i > $weekstart ) {
				$after[] = $weekday;
			} elseif ( $i < $weekstart ) {
				$before[] = $weekday;
			}
		}

		// --- put the days before the start day at the end ---
		$weekdays = array_merge( $start, $after );
		$weekdays = array_merge( $weekdays, $before );

		// --- filter and return ---
		$weekdays = apply_filters( 'schedule_engine_schedule_weekdays', $weekdays, $weekstart, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$weekdays = apply_filters( $context . '_schedule_weekdays', $weekdays, $weekstart, $channel );
		}
		return $weekdays;
	}

	// ----------------------
	// Get Schedule Weekdates
	// ----------------------
	// 2.3.0: added for date based calculations
	public function get_schedule_weekdates( $weekdays, $time = false, $timezone = false ) {

		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- maybe get current time ---
		$time = $time ? $time : $this->get_now();
		$timezone = $timezone ? $timezone : $this->get_timezone();

		// 2.3.2: use timezone setting to get offset date
		if ( defined( 'SCHEDULE_ENGINE_USE_SERVER_TIMES' ) && SCHEDULE_ENGINE_USE_SERVER_TIMES ) {
			$today = date( 'l', $time );
		} else {
			// 2.3.3.9: fix to use get_time
			$today = $this->get_time( 'day', $time, $timezone );
		}

		// --- get weekday index for today ---
		$weekdates = array();
		foreach ( $weekdays as $i => $weekday ) {
			if ( $weekday == $today ) {
				$index = $i;
			}
		}
		foreach ( $weekdays as $i => $weekday ) {

			$diff = $i - $index;

			// 2.5.7: use datetime object and modify
			$times = $this->get_times( $time, $timezone );
			$date_time = $times['object'];
			// $weekdate_time = $time - ( $diff * 24 * 60 * 60 );
			if ( 0 == $diff ) {
				$weekdate_time = $date_time->format( 'U');
			} elseif ( $diff > 0 ) {
				$date_time->modify( '+' . $diff . ' day' );
				$weekdate_time = $date_time->format( 'U');
			} elseif ( $diff < 0 ) {
				$date_time->modify( (string) $diff . ' day' );
				$weekdate_time = $date_time->format( 'U');
			}
			// 2.3.2: include timezone adjustment
			if ( defined( 'SCHEDULE_ENGINE_USE_SERVER_TIMES' ) && SCHEDULE_ENGINE_USE_SERVER_TIMES ) {
				$weekdate = date( 'Y-m-d', $weekdate_time );
			} else {
				// 2.3.3.9: fix to use get_time
				$weekdate = $this->get_time( 'Y-m-d', $weekdate_time, $timezone );
			}
			$weekdates[$weekday] = $weekdate;
		}
		// echo '**A**'; print_r( $weekdates );

		// 2.4.0.4: check/fix for duplicate date crackliness (daylight saving?)
		foreach ( $weekdates as $day => $date ) {
			if ( isset( $prevdate ) && ( $prevdate == $date ) ) {
				$weekdates[$day] = $this->get_next_date( $date );
				$found = false;
				foreach ( $weekdates as $k => $v ) {
					if ( $found ) {
						$weekdates[$k] = $this->get_next_date( $v );
					}
					if ( $k == $day ) {
						$found = true;
					}
				}
			}
			$prevdate = $date;
		}
		// echo '**B**'; print_r( $weekdates );

		// --- filter and return ---
		$weekdates = apply_filters( 'schedule_engine_schedule_weekdates', $weekdates, $weekdays, $time, $timezone, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$weekdates = apply_filters( $context . '_schedule_weekdates', $weekdates, $weekdays, $time, $timezone, $channel );
		}
		
		return $weekdates;
	}

	// ------------
	// Get Next Day
	// ------------
	// 2.3.0: added get next day helper
	public function get_next_day( $day ) {
		// note: for internal use so not translated
		$day = trim( $day );
		if ( 'Sunday' == $day ) {
			return 'Monday';
		}
		if ( 'Monday' == $day ) {
			return 'Tuesday';
		}
		if ( 'Tuesday' == $day ) {
			return 'Wednesday';
		}
		if ( 'Wednesday' == $day ) {
			return 'Thursday';
		}
		if ( 'Thursday' == $day ) {
			return 'Friday';
		}
		if ( 'Friday' == $day ) {
			return 'Saturday';
		}
		if ( 'Saturday' == $day ) {
			return 'Sunday';
		}

		return '';
	}

	// ----------------
	// Get Previous Day
	// ----------------
	// 2.3.0: added get previous day helper
	public function get_previous_day( $day ) {
		// note: for internal use so not translated
		$day = trim( $day );
		if ( 'Sunday' == $day ) {
			return 'Saturday';
		}
		if ( 'Monday' == $day ) {
			return 'Sunday';
		}
		if ( 'Tuesday' == $day ) {
			return 'Monday';
		}
		if ( 'Wednesday' == $day ) {
			return 'Tuesday';
		}
		if ( 'Thursday' == $day ) {
			return 'Wednesday';
		}
		if ( 'Friday' == $day ) {
			return 'Thursday';
		}
		if ( 'Saturday' == $day ) {
			return 'Friday';
		}

		return '';
	}

	// -------------
	// Get Next Date
	// -------------
	// 2.3.2: added for more reliable calculations
	public function get_next_date( $date, $weekday = false ) {

		// note: this is used internally so timezone not needed
		$timedate = strtotime( $date );
		$timedate = $timedate + ( 24 * 60 * 60 );
		if ( $weekday ) {
			$day = $this->get_time( 'l', $timedate );
			if ( $day != $weekday ) {
				$i = 0;
				while ( $day != $weekday ) {
					$timedate = $timedate + ( 24 * 60 * 60 );
					$day = strtotime( 'l', $timedate );
					if ( 8 == $i ) {
						// - failback for while failure -
						$timedate = strtotime( $date );
						$next_date = date( 'next ' . $weekday, $timedate );
						return $next_date;
					}
					$i++;
				}
			}
		}
		$next_date = $this->get_time( 'Y-m-d', $timedate );
		return $next_date;
	}

	// -----------------
	// Get Previous Date
	// -----------------
	// 2.3.2: added for more reliable calculations
	public function get_previous_date( $date, $weekday = false ) {

		// note: this is used internally so timezone not used
		$timedate = strtotime( $date );
		$timedate = $timedate - ( 24 * 60 * 60 );
		if ( $weekday ) {
			$day = date( 'l', $timedate );
			if ( $day != $weekday ) {
				$i = 0;
				while ( $day != $weekday ) {
					$timedate = $timedate - ( 24 * 60 * 60 );
					$day = strtotime( 'l', $timedate );
					if ( 8 == $i ) {
						// - failback for while failure -
						$timedate = strtotime( $date );
						$previous_date = date( 'previous ' . $weekday, $timedate );
						return $previous_date;
					}
					$i++;
				}
			}
		}
		$previous_date = date( 'Y-m-d', $timedate );
		return $previous_date;
	}

	// ---------------------------
	// Convert Hour to Time Format
	// ---------------------------
	// (note: used with suffix for on-the-hour times)
	// 2.3.0: standalone function via master-schedule-default.php
	// 2.3.0: optionally add suffix for both time formats
	public function convert_hour( $hour, $timeformat = 24, $suffix = true ) {

		$hour = intval( $hour );

		// 2.3.0: handle next and previous hours (over 24 or below 0)
		if ( $hour < 0 ) {
			while ( $hour < 0 ) {
				$hour = $hour + 24;
			}
		}
		if ( $hour > 24 ) {
			while ( $hour > 24 ) {
				$hour = $hour - 24;
			}
		}

		if ( 24 === (int) $timeformat ) {
			// --- 24 hour time format ---
			if ( 24 == $hour ) {
				$hour = '00';
			} elseif ( $hour < 10 ) {
				$hour = '0' . $hour;
			}
			if ( $suffix ) {
				$hour .= ':00';
			}
		} elseif ( 12 === (int) $timeformat ) {
			// --- 12 hour time format ---
			// 2.2.7: added meridiem translations
			if ( ( $hour === 0 ) || ( 24 === $hour ) ) {
				// midnight
				$hour = '12';
				if ( $suffix ) {
					$hour .= ' ' . $this->translate_meridiem( 'am' );
				}
			} elseif ( $hour < 12 ) {
				// morning
				if ( $suffix ) {
					$hour .= ' ' . $this->translate_meridiem( 'am' );
				}
			} elseif ( 12 === $hour ) {
				// noon
				if ( $suffix ) {
					$hour .= ' ' . $this->translate_meridiem( 'pm' );
				}
			} elseif ( $hour > 12 ) {
				// after-noon
				$hour = $hour - 12;
				if ( $suffix ) {
					$hour .= ' ' . $this->translate_meridiem( 'pm' );
				}
			}
		}
		// 2.3.2: fix for possible double spacing crackliness
		$hour = str_replace( '  ', ' ', $hour );

		return $hour;
	}

	// ----------------------------
	// Convert Shift to Time Format
	// ----------------------------
	// 2.3.0: added to convert shift time to 24 hours (or back)
	public function convert_shift_time( $time, $timeformat = 24 ) {

		// note: timezone can be ignored here as just getting hours and minutes
		// 2.3.3.9: added space between date and time
		$timestamp = strtotime( $this->get_time( 'Y-m-d' ) . ' ' . $time );
		if ( 12 == (int) $timeformat ) {
			$time = $this->get_time( 'g:i a', $timestamp );
			// 2.5.6: removed translation here as for data not output
			// str_replace( 'am', $this->translate_meridiem( 'am' ), $time );
			// str_replace( 'pm', $this->translate_meridiem( 'pm' ), $time );
		} elseif ( 24 == (int) $timeformat ) {
			$time = $this->get_time( 'H:i', $timestamp );
		}

		return $time;
	}


	// --------------------
	// === Translations ===
	// --------------------

	// ----------
	// Get Locale
	// ----------
	public function get_locale() {
		
		// --- get channel and context ---
		$channel = $this->channel;
		$context = $this->context;

		// --- get set locale ---
		$locale = $this->locale;

		// --- filter and return ---
		$locale = apply_filters( 'schedule_engine_locale', $locale, $channel, $context );
		if ( ( '' != $context ) && is_string( $context ) ) {
			$locale = apply_filters( $context . '_locale', $locale, $channel );
		}
		return $locale;
	}

	// --------------
	// Get All Months
	// --------------
	public function get_months( $translate = false ) {
		$months = array(
			'January',
			'February',
			'March',
			'April',
			'May',
			'June',
			'July',
			'August',
			'September',
			'October',
			'November',
			'December',
		);
		if ( $translate ) {
			foreach ( $months as $i => $month ) {
				$months[$i] = $this->translate_month( $month );
			}
		}
		return $months;
	}
		
	// ------------
	// Get All Days
	// ------------
	public function get_weekdays( $translate = false ) {
		$days = array(
			'Sunday',
			'Monday',
			'Tuesday',
			'Wednesday',
			'Thursday',
			'Friday',
			'Saturday'
		);
		if ( $translate ) {
			foreach ( $days as $i => $day ) {
				$days[$i] = $this->translate_weekday( $day );
			}
		}
		return $days;
	}

	// -------------
	// Get All Hours
	// -------------
	public function get_hours( $format = 24, $translate = false ) {
		$hours = array();
		if ( 24 === (int) $format ) {
			$hours = array( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 );
		} elseif ( 12 === (int) $format ) {
			$hours = array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 );
		}
		if ( $translate ) {
			foreach ( $hours as $i => $hour ) {
				$hours[$i] = number_format_i18n( $hour );	
			}
		}
		return $hours;
	}

	// ---------------
	// Get All Minutes
	// ---------------
	public function get_minutes( $translate = false ) {

		$mins = array();
		for ( $i = 0; $i < 60; $i++ ) {
			if ( $i < 10 ) {
				$min = '0' . $i;
			} else {
				$min = $i;
			}
			$mins[$i] = $min;
		}
		if ( $translate ) {
			foreach ( $mins as $i => $min ) {
				$mins[$i] = number_format_i18n( $min );	
			}
		}
		return $mins;
	}

	// -----------------
	// Translate Weekday
	// -----------------
	// important note: translated individually as cannot translate a variable
	// 2.2.7: use wp locale class to translate weekdays
	// 2.3.0: allow for abbreviated and long version changeovers
	// 2.3.2: default short to null for more flexibility
	public function translate_weekday( $weekday, $short = null ) {

		// 2.3.0: return empty for empty select option
		if ( empty( $weekday ) ) {
			return '';
		}

		// --- get weekdays ---
		$days = $this->get_weekday();

		// --- maybe switch locales ---
		// 2.5.0: added for possible locale switching
		$locale = $this->locale;
		if ( '' != $locale ) {
			$switcher = new WP_Locale_Switcher();
			$switcher->switch_to_locale( $locale );
		}
		global $wp_locale;

		// --- translate weekday ---
		// 2.3.2: optimized weekday translations
		foreach ( $days as $i => $day ) {
			$abbr = substr( $day, 0, 3 );
			if ( ( $weekday == $day ) || ( $weekday == $abbr ) ) {
				if ( ( !$short && !is_null( $short ) ) || ( is_null( $short ) && ( $weekday == $day ) ) ) {
					$weekday = $wp_locale->get_weekday( $i );
				} elseif ( $short || ( is_null( $short ) && ( $weekday == $abbr ) ) ) {
					$weekday = $wp_locale->get_weekday_abbrev( $wp_locale->get_weekday( $i ) );
				}
			}
		}

		// --- fallback if day number supplied ---
		// 2.3.2: optimized day number fallback
		if ( !isset ($weekday ) ) {
			$daynum = intval( $weekday );
			if ( ( $daynum > -1 ) && ( $daynum < 7 ) ) {
				if ( $short ) {
					$weekday = $wp_locale->get_weekday_abbrev( $wp_locale->get_weekday( $daynum ) );
				} else {
					$weekday = $wp_locale->get_weekday( $daynum );
				}
			}
		}

		// --- maybe switch back to original locale ---
		// 2.5.0: added for possible locale switching
		if ( '' != $locale ) {
			$switcher->restore_current_locale();
		}
		return $weekday;
	}

	// ----------------
	// Replace Weekdays
	// ----------------
	// 2.3.2: to replace with translated weekdays in a time string
	public function replace_weekday( $string ) {

		$days = $this->get_weekday();
		foreach( $days as $day ) {
			$abbr = substr( $day, 0, 3 );
			if ( strstr( $string, $day ) ) {
				$translated = $this->translate_weekday( $day );
				$string = str_replace( $day, $translated, $string );
			} elseif ( strstr( $string, $abbr ) ) {
				$translated = $this->translate_weekday( $abbr );
				$string = str_replace( $abbr, $translated, $string );
			}
		}

		return $string;
	}

	// ---------------
	// Translate Month
	// ---------------
	// important note: translated individually as cannot translate a variable
	// 2.2.7: use wp locale class to translate months
	// 2.3.2: default short to null for more flexibility
	public function translate_month( $month, $short = null ) {

		// 2.3.0: return empty for empty select option
		if ( empty( $month ) ) {
			return '';
		}

		// --- get months ---
		$months = $this->get_month();

		// --- maybe switch locales ---
		// 2.5.0: added for possible locale switching
		$locale = $this->locale;
		if ( '' != $locale ) {
			$switcher = new WP_Locale_Switcher();
			$switcher->switch_to_locale( $locale );
		}
		global $wp_locale;

		// --- translate month ---
		// 2.3.2: optimized month translations
		foreach ( $months as $i => $fullmonth ) {
			$abbr = substr( $fullmonth, 0, 3 );
			if ( ( $month == $fullmonth ) || ( $month == $abbr ) ) {
				if ( ( !$short && !is_null( $short ) ) || ( is_null( $short ) && ( $month == $fullmonth ) ) ) {
					$month = $wp_locale->get_month( ( $i + 1 ) );
				} elseif ( $short || ( is_null( $short ) && ( $month == $abbr ) ) ) {
					// 2.5.6: fix to match month not weekday in condition above
					$month = $wp_locale->get_month_abbrev( $wp_locale->get_month( ( $i + 1 ) ) );
				}
			}
		}

		// --- fallback if month number supplied ---
		// 2.3.2: optimized month number fallback
		if ( !isset( $month ) ) {
			$monthnum = intval( $month );
			if ( ( $monthnum > 0 ) && ( $monthnum < 13 ) ) {
				if ( $short ) {
					$month = $wp_locale->get_month_abbrev( $wp_locale->get_month( $monthnum ) );
				} else {
					$month = $wp_locale->get_month( $monthnum );
				}
			}
		}

		// --- maybe switch back to original locale ---
		// 2.5.0: added for possible locale switching
		if ( '' != $locale ) {
			$switcher->restore_current_locale();
		}
		return $month;
	}

	// --------------
	// Replace Months
	// --------------
	// 2.3.2: to replace with translated months in a time string
	public function replace_month( $string ) {

		$months = $this->get_month();
		foreach( $months as $month ) {
			$abbr = substr( $month, 0, 3 );
			if ( strstr( $string, $month ) ) {
				$translated = $this->translate_month( $month );
				$string = str_replace( $month, $translated, $string );
			} elseif ( strstr( $string, $abbr ) ) {
				$translated = $this->translate_month( $abbr );
				$string = str_replace( $abbr, $translated, $string );
			}
		}

		return $string;
	}

	// ------------------
	// Translate Meridiem
	// ------------------
	// 2.2.7: added meridiem translation function
	public function translate_meridiem( $meridiem ) {

		// --- maybe switch locales ---
		// 2.5.0: added for possible locale switching
		$locale = $this->locale;
		if ( '' != $locale ) {
			$switcher = new WP_Locale_Switcher();
			$switcher->switch_to_locale( $locale );
		}

		// --- get translated meridiem ---
		global $wp_locale;
		$meridiem = $wp_locale->get_meridiem( $meridiem );

		// --- maybe switch back to original locale ---
		// 2.5.0: added for possible locale switching
		if ( '' != $locale ) {
			$switcher->restore_current_locale();
		}

		return $meridiem;
	}

	// ----------------
	// Replace Meridiem
	// ----------------
	// 2.3.2: added optimized meridiem replacement
	public function replace_meridiem( $string ) {

		if ( strstr( $string, 'am' ) ) {
			$am = $this->translate_meridiem( 'am' );
			$string = str_replace( 'am', $am, $string );
		}
		if ( strstr( $string, 'pm' ) ) {
			$pm = $this->translate_meridiem( 'pm' );
			$string = str_replace( 'pm', $pm, $string );
		}
		if ( strstr( $string, 'AM' ) ) {
			$am = $this->translate_meridiem( 'AM' );
			$string = str_replace( 'AM', $am, $string );
		}
		if ( strstr( $string, 'PM' ) ) {
			$pm = $this->translate_meridiem( 'PM' );
			$string = str_replace( 'PM', $pm, $string );
		}

		return $string;
	}

	// ---------------------
	// Translate Time String
	// ---------------------
	// 2.3.2: replace with translated month, day and meridiem in a string
	public function translate_time( $string ) {

		$string = $this->replace_meridiem( $string );
		$string = $this->replace_weekday( $string );
		$string = $this->replace_month( $string );

		return $string;
	}


	// -----------------
	// === Debugging ===
	// -----------------

	// -----------------------
	// Write to Debug Log File
	// -----------------------
	public function debug_log( $data, $echo = true, $filename = false ) {

		// --- maybe output debug info ---
		if ( $echo ) {
			echo '<span class="schedule-engine-debug" style="display:none;">' . esc_html( rtrim( $data ) ) . '</span>' . PHP_EOL;
		}

		// --- check for logging constant ---
		if ( !$filename && $this->debug_log ) {
			$filename = 'schedule-engine.log';
		} elseif ( false === $this->debug_log ) {
			$filename = false;
		}

		// --- maybe write to file ---
		if ( $filename ) {

			// --- maybe create debug directory ---
			if ( !is_dir( dirname( __FILE__ ) . '/debug' ) ) {
				wp_mkdir_p( dirname( __FILE__ ) . '/debug' );
			}

			// --- ensure line break at end of debug line ---
			$data = rtrim( $data ) . PHP_EOL;

			// --- write to debug file path ---
			$filepath = dirname( __FILE__ ) . '/debug/' . $filename;
			error_log( $data, 3, $filepath );
		}
	}

// --- close schedule engine class ---
}

