<?php

if (!defined('ABSPATH')) die('No direct access allowed');

if (!class_exists('WP_Optimize_Cron_Scheduler')) {

class WP_Optimize_Cron_Scheduler {

	/**
	 * Holds an instance of semaphore lock class
	 *
	 * @var object $_lock
	 */
	protected static $_lock = null;

	/**
	 * Adds a method to run when premium cron event fires
	 */
	private function __construct() {
		$this->maybe_remove_unsupported_table_optimization_crons();
		add_action('wpo_cron_event3', array($this, 'cron_action'), 10, 2);
	}

	/**
	 * Returns singleton instance of this class
	 *
	 * @return WP_Optimize_Cron_Scheduler Singleton Instance
	 */
	public static function get_instance() {
		static $instance = null;
		if (null === $instance) {
			$instance = new self();
		}
		return $instance;
	}

	/**
	 * Executed this function on cron event
	 *
	 * @param  string $cron_id  ID of scheduled cron
	 * @param  string $schedule Name of scheduled cron
	 * @return void
	 */
	public function cron_action($cron_id, $schedule) {

		$options = WP_Optimize()->get_options();
		WP_Optimize()->log('WPO: Starting cron_action()');
		$auto_options = $options->get_option('auto');
		$auto_updated_options = $options->get_option('auto-updated');
		$auto_backup_enabled = $options->get_option('enable-auto-backup-scheduled');
		
		$options->update_option('last-optimized', time());
		// Currently the output of the optimizations is not saved/used/logged.
		$optimizer = WP_Optimize()->get_optimizer();
		$tablesstatus = $optimizer->get_table_information();
		$does_server_allow_table_optimization = WP_Optimize()->get_server_compatibility_instance()->does_server_allow_table_optimization();


		if ($this->set_semaphore_lock()) {

			if ('true' === $auto_backup_enabled) {
				do_action('updraft_backup_database');
			}
			
			foreach ($auto_options[$cron_id]['optimization'] as $optimization_id) {
				if ('optimizetables' === $optimization_id) {

					if (false === $does_server_allow_table_optimization) {
						WP_Optimize()->log('WPO: Skipping table optimization - not allowed on this server');
						continue;
					}

					// Add message to log about innoDB tables, if needed.
					if (false === $tablesstatus['is_optimizable'] && $tablesstatus['inno_db_tables'] > 0) {
						$force_optimization = 'true' === $options->get_option('auto-innodb');
						if ($force_optimization) {
							WP_Optimize()->log('WPO: Chooses to optimize InnoDB tables');
						} else {
							WP_Optimize()->log('WPO: Chooses to not optimize InnoDB tables');
						}
						$optimizer->do_optimization($optimizer->get_optimization($optimization_id, array('force' => $force_optimization)));
					} else {
						$optimizer->do_optimization($optimization_id);
					}
				} else {
					$optimizer->do_optimization($optimization_id);
				}
			}
			$this->release_semaphore_lock();
		}

		// Remove once event from options table
		if (false !== strpos($schedule, 'wpo_once')) {
			unset($auto_updated_options[$cron_id]);
			$options->update_option('auto-updated', $auto_updated_options);
		}

		// call actions after optimisation. used to send message in email logger.
		do_action('wp_optimize_after_optimizations');
	}


	/**
	 * Returns next event timestamp
	 *
	 * @return string | false
	 */
	public function wpo_cron_next_event() {
		$cron_events = get_option('cron');
		ksort($cron_events);
		$wpo_cron_event3 = array();
		foreach ($cron_events as $timestamp => $schedule) {
			if (!is_array($schedule)) continue;
			foreach ($schedule as $key => $value) {
				if ('wpo_cron_event3' == $key) {
					$wpo_cron_event3[$timestamp][$key] = $value;
				}
			}
		}

		$keys = array_keys($wpo_cron_event3);
		if (!empty($keys)) {
			return $keys[0];
		}
		return false;
	}

	/**
	 * Activate WPO auto events
	 *
	 * @return void
	 */
	public function cron_activate() {
		$options = WP_Optimize()->get_options();

		$auto_options = $options->get_option('auto-updated');

		if (!is_array($auto_options)) {
			$auto_options = array();
		}
		$new_auto_options = array();
		$cron_id = 0;
		foreach ($auto_options as $event) {
			if ('0' === (string) $event['status']) continue;
			$new_auto_options[$cron_id] = $event;
			WP_Optimize()->log($event['schedule_type']);
			switch ($event['schedule_type']) {
				case 'wpo_once':
					$this->set_once_cron($event, $cron_id);
					break;
				case 'wpo_daily':
					$this->set_daily_cron($event, $cron_id);
					break;
				case 'wpo_weekly':
					$this->set_weekly_cron($event, $cron_id);
					break;
				case 'wpo_fortnightly':
					$this->set_fortnightly_cron($event, $cron_id);
					break;
				case 'wpo_monthly':
					$this->set_monthly_cron($event, $cron_id);
					break;
			}
			$cron_id++;
		}
		$options->update_option('auto', $new_auto_options);

	}

	/**
	 * Activate daily events
	 *
	 * @param  array   $event   Details of event
	 * @param  integer $cron_id ID of cron schedule
	 * @return void
	 */
	private function set_daily_cron($event, $cron_id) {
		$selected_schedule = "wpo_daily";

		$cron_schedule_user_date_time = strtotime(gmdate("Y-m-d") . ' ' . $event['time']);
		$gmt_offset = HOUR_IN_SECONDS * get_option('gmt_offset');
		$cron_schedule_date_time = $cron_schedule_user_date_time - $gmt_offset;

		if ($cron_schedule_date_time < time()) {
			$cron_schedule_date_time += DAY_IN_SECONDS;
		}

		wp_schedule_event($cron_schedule_date_time, $selected_schedule, 'wpo_cron_event3', array($cron_id, $selected_schedule));
		WP_Optimize()->log('wpo_cron_event3 - ' . $cron_id . ' - ' . $selected_schedule);
	}

	/**
	 * Activate weekly events
	 *
	 * @param  array   $event   Details of event
	 * @param  integer $cron_id ID of cron schedule
	 * @return void
	 */
	private function set_weekly_cron($event, $cron_id) {
		$selected_schedule = "wpo_weekly";

		$user_day_number = $event['day'];

		// Need to match between $wp_locale->get_weekday() and php's gmdate('N')
		if (1 === $user_day_number) {
			$user_day_number = 7;
		} else {
			$user_day_number--;
		}

		$today_day_number = gmdate('N');
		$cron_schedule_user_date_time = strtotime(gmdate("Y-m-d") . ' ' . $event['time']);
		$week_offset = ($user_day_number - $today_day_number) * DAY_IN_SECONDS;
		$gmt_offset = HOUR_IN_SECONDS * get_option('gmt_offset');
		$cron_schedule_date_time = $cron_schedule_user_date_time - $gmt_offset + $week_offset;

		if ($cron_schedule_date_time < time()) {
			$cron_schedule_date_time += WEEK_IN_SECONDS;
		}

		wp_schedule_event($cron_schedule_date_time, $selected_schedule, 'wpo_cron_event3', array($cron_id, $selected_schedule));
		WP_Optimize()->log('wpo_cron_event3 - ' . $cron_id . ' - ' . $selected_schedule);
	}


	/**
	 * Activate fortnightly events
	 *
	 * @param  array   $event   Details of event
	 * @param  integer $cron_id ID of cron schedule
	 * @return void
	 */
	private function set_fortnightly_cron($event, $cron_id) {
		$selected_schedule = "wpo_fortnightly";

		$user_week_number = $event['week'];
		$user_day_number = $event['day'];
		$today_day_number = gmdate('N');

		// Need to match between $wp_locale->get_weekday() and php's gmdate('N')
		if (1 === $user_day_number) {
			$user_day_number = 7;
		} else {
			$user_day_number--;
		}

		$cron_schedule_user_date_time = strtotime(gmdate("Y-m-d") . ' ' . $event['time']);
		$week_offset = ($user_day_number - $today_day_number) * DAY_IN_SECONDS;
		$gmt_offset = HOUR_IN_SECONDS * get_option('gmt_offset');
		$cron_schedule_date_time = $cron_schedule_user_date_time - $gmt_offset + $week_offset;

		if ($cron_schedule_date_time < time() || '2nd' == $user_week_number) {
			$cron_schedule_date_time += WEEK_IN_SECONDS;
		}

		wp_schedule_event($cron_schedule_date_time, $selected_schedule, 'wpo_cron_event3', array($cron_id, $selected_schedule));
		WP_Optimize()->log('wpo_cron_event3 - ' . $cron_id . ' - ' . $selected_schedule);
	}

	/**
	 * Activate monthly events
	 *
	 * @param  array   $event   Details of event
	 * @param  integer $cron_id ID of cron schedule
	 * @return void
	 */
	private function set_monthly_cron($event, $cron_id) {
		$selected_schedule = 'wpo_monthly';

		$user_day_number = min($event['day_number'], gmdate("t"));
		$schedule_day_number = $user_day_number;
		$cron_schedule_user_date_time = strtotime(gmdate("Y-m-" . $schedule_day_number) . ' ' . $event['time']);
		$gmt_offset = HOUR_IN_SECONDS * get_option('gmt_offset');
		$cron_schedule_date_time = $cron_schedule_user_date_time - $gmt_offset;

		wp_schedule_event($cron_schedule_date_time, $selected_schedule, 'wpo_cron_event3', array($cron_id, $selected_schedule));
		WP_Optimize()->log('wpo_cron_event3 - ' . $cron_id . ' - ' . $selected_schedule);
	}

	/**
	 * Activate once (one off) events
	 *
	 * @param  array   $event   Details of event
	 * @param  integer $cron_id ID of cron schedule
	 * @return void
	 */
	private function set_once_cron($event, $cron_id) {
		$cron_schedule_user_date_time_human = $event['date'] . ' ' . $event['time'];
		$cron_schedule_user_date_time = strtotime($cron_schedule_user_date_time_human);

		$gmt_offset = HOUR_IN_SECONDS * get_option('gmt_offset');
		$cron_schedule_date_time = $cron_schedule_user_date_time - $gmt_offset;
		wp_schedule_single_event($cron_schedule_date_time, 'wpo_cron_event3', array($cron_id, 'wpo_once'));
		WP_Optimize()->log('wpo_cron_event3 - once');
	}

	/**
	 * Using semaphore lock before starting optimization
	 *
	 * @param string $semaphore Name of semaphore lock
	 *
	 * @return boolean Semaphore lock status
	 */
	public function set_semaphore_lock($semaphore = 'wpo') {
		// Are we doing an action called by the WP scheduler? If so, we want to check when that last happened; the point being that the dodgy WP scheduler, when overloaded, can call the event multiple times - and sometimes, it evades the semaphore because it calls a second run after the first has finished, or > 3 minutes (our semaphore lock time) later
		// doing_action() was added in WP 3.9
		// wp_cron() can be called from the 'init' action
		$seconds_ago = time();
		if (function_exists('doing_action') && (doing_action('init') || constant('DOING_CRON')) && doing_action('wpo_cron_event3')) {
			$last_scheduled_action_called_at = get_option("wpo_last_scheduled_$semaphore");
			// 11 minutes - so, we're assuming that they haven't custom-modified their schedules to run scheduled optimizations more often than that. If they have, they need also to use the filter to override this check.
			$seconds_ago = time() - $last_scheduled_action_called_at;
			if ($last_scheduled_action_called_at && $seconds_ago < 660 && apply_filters('wpo_check_repeated_scheduled_optimizations', true)) {
				WP_Optimize()->log(sprintf('Scheduled optimization aborted - another optimization of this type was apparently invoked by the WordPress scheduler only %d seconds ago - the WordPress scheduler invoking events multiple times usually indicates a very overloaded server (or other plugins that mis-use the scheduler)', $seconds_ago));
				return false;
			}
		}
		update_option("wpo_last_scheduled_$semaphore", time());

		self::$_lock = new Updraft_Semaphore_3_0($semaphore);
		
		// loggers for semaphore
		if (!empty(WP_Optimize()->wpo_loggers())) {
			self::$_lock->set_loggers(WP_Optimize()->wpo_loggers());
		}

		$semaphore_log_message = 'Requesting semaphore lock ('.$semaphore.')';
		if (!empty($last_scheduled_action_called_at)) {
			$semaphore_log_message .= " (apparently via scheduler: last_scheduled_action_called_at=$last_scheduled_action_called_at, seconds_ago=$seconds_ago)";
		} else {
			$semaphore_log_message .= " (apparently not via scheduler)";
		}

		WP_Optimize()->log($semaphore_log_message);
		if (!self::$_lock->lock()) {
			WP_Optimize()->log('Failed to gain semaphore lock ('.$semaphore.') - another optimization of this type is apparently already active - aborting');
			return false;
		}
		return true;
	}

	/**
	 * Releases semaphore lock after optimization is complete.
	 */
	public function release_semaphore_lock() {
		if (!empty(self::$_lock)) self::$_lock->release();
	}

	/**
	 * Deactivate WPO events by clearing the hook
	 *
	 * @return void
	 */
	public function wpo_cron_deactivate() {
		$auto_options = WP_Optimize()->get_options()->get_option('auto');
		foreach ($auto_options as $cron_id => $event) {
			if (!isset($event['schedule_type'])) continue;
			wp_clear_scheduled_hook('wpo_cron_event3', array($cron_id, $event['schedule_type']));
		}
	}

	/**
	 * Remove scheduled table-optimization crons if the server does not allow it.
	 *
	 * @return void
	 */
	private function maybe_remove_unsupported_table_optimization_crons(): void {
		$does_server_allow_table_optimization = WP_Optimize()->get_server_compatibility_instance()->does_server_allow_table_optimization();

		if ($does_server_allow_table_optimization) {
			return;
		}

		$options = WP_Optimize()->get_options();
		$auto_updated_options = $options->get_option('auto-updated');

		if (empty($auto_updated_options) || !is_array($auto_updated_options)) {
			return;
		}

		foreach ($auto_updated_options as $cron_id => $event) {
			if (empty($event['optimization']) || !is_array($event['optimization'])) {
				continue;
			}

			if (!in_array('optimizetables', $event['optimization'], true)) {
				continue;
			}

			$event['optimization'] = array_values(
				array_diff($event['optimization'], array('optimizetables'))
			);

			// If no optimizations remain, remove the cron entirely
			if (empty($event['optimization'])) {

				unset($auto_updated_options[$cron_id]);

			} else {
				// Otherwise, keep the cron but update optimizations
				$auto_updated_options[$cron_id]['optimization'] = $event['optimization'];
			}
		}

		if ($options->update_option('auto-updated', $auto_updated_options)) {
			$this->wpo_cron_deactivate();
			$this->cron_activate();
		}
	}
}

}
