<?php
/** 
 * @package   	VikBooking - Libraries
 * @subpackage 	system
 * @author    	E4J s.r.l.
 * @copyright 	Copyright (C) 2018 E4J s.r.l. All Rights Reserved.
 * @license  	http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link 		https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * Class used to handle the cron jobs execution.
 *
 * @since 1.5.10
 */
class VikBookingCron
{
	/**
	 * Schedules the published cron jobs to be executed according
	 * to the selected recurrence.
	 * 
	 * @return 	void
	 */
	public static function setup()
	{
		static $setup = false;

		if ($setup)
		{
			// setup is allowed only once
			return;
		}

		// prevent double setup
		$setup = true;

		/**
		 * Filters the non-default cron schedules.
		 * Adds support to repeat the cron jobs every 5 minutes, every half hour,
		 * every 2 hours, and every month.
		 *
		 * @param 	array 	$schedules 	An array of non-default cron schedules.
		 */
		add_filter('cron_schedules', function($schedules)
		{
			// add support for "every 5 minutes" recurrence
			$schedules['every_5_minutes'] = array(
				'interval' => MINUTE_IN_SECONDS * 5,
				'display'  => __('Every 5 Minutes', 'vikbooking'),
			);

			// add support for "every 15 minutes" recurrence
			$schedules['every_15_minutes'] = array(
				'interval' => MINUTE_IN_SECONDS * 15,
				'display'  => __('Every 15 Minutes', 'vikbooking'),
			);

			// add support for "every 30 minutes" recurrence
			$schedules['half_hour'] = array(
				'interval' => HOUR_IN_SECONDS / 2,
				'display'  => __('Every Half Hour', 'vikbooking'),
			);

			// add support for "every 2 hours" recurrence
			$schedules['every_2_hours'] = array(
				'interval' => HOUR_IN_SECONDS * 2,
				'display'  => __('Every 2 Hours', 'vikbooking'),
			);

			// add support for "every month" recurrence
			$schedules['monthly'] = array(
				'interval' => MONTH_IN_SECONDS,
				'display'  => __('Monthly', 'vikbooking'),
			);

			return $schedules;
		});

		/**
		 * Trigger event to allow the plugins to include custom HTML within the view. 
		 * It is possible to return an associative array to group the HTML strings
		 * under different fieldsets. Plain/html string will be always pushed within
		 * the "custom" fieldset instead.
		 *
		 * Displays the field to select the cron job recurrence.
		 *
		 * @param 	mixed   $forms  The HTML to display.
		 * @param 	mixed   $view 	The current view instance.
		 *
		 * @since 	1.5.10
		 */
		add_filter('vikbooking_display_view_cron', array('VikBookingCron', 'addScheduleControlForm'), 10, 2);

		/**
		 * Trigger event to allow the plugins to make something before saving
		 * a record in the database. Used to extend the cron jobs management
		 * in order to support the selection of the recurrence.
		 *
		 * @param 	bool   $save  False to abort the saving process.
		 * @param 	array  $args  The saved record.
		 *
		 * @since 	1.5.10
		 */
		add_filter('vikbooking_before_save_cronjob', array('VikBookingCron', 'saveScheduleControl'), 10, 2);

		/**
		 * Trigger event to allow the plugins to make something after saving
		 * a record in the database. Used to schedule the cron job execution.
		 *
		 * @param 	mixed  $status  The saving status.
		 * @param 	array  $args    The saved record.
		 * @param 	bool   $is_new  True in case of insert.
		 *
		 * @since 	1.5.10
		 */
		add_filter('vikbooking_after_save_cronjob', array('VikBookingCron', 'checkSchedulingAfterSave'), 10, 3);

		/**
		 * Trigger event to allow the plugins to make something after publishing or
		 * unpublishing one or more records. Used to unschedule the unpublished cron jobs.
		 *
		 * @param 	bool   $delete  False to abort the deleting process.
		 * @param 	array  $ids     An array of IDs to delete.
		 *
		 * @since 	1.5.10
		 */
		add_filter('vikbooking_before_delete_cronjob', array('VikBookingCron', 'checkSchedulingBeforeDelete'), 10, 2);

		// fetch list of cron jobs
		$crons = static::getJobs();

		if (!$crons)
		{
			// nothing to execute
			return;
		}

		$model = VBOMvcModel::getInstance('cronjob');

		// iterate cron jobs list
		foreach ($crons as $id_cron)
		{
			// get cron job details
			$cron = $model->getItem($id_cron);

			if (!$cron)
			{
				// cron not found, go ahead
				continue;
			}

			// fetch recurrence interval
			$interval = !empty($cron->schedule_key) ? $cron->schedule_key : 'daily';

			// build cron listener hook
			$hook = static::getScheduleHook($cron);

			/**
			 * Action used to execute all the cron jobs that have been created 
			 * through the VikBooking panel. Only published cron jobs can
			 * be executed.
			 *
			 * The scheduling of this hook must be registered through
			 * WordPress in order to be executed.
			 */
			add_action($hook, function() use ($id_cron)
			{
				VikBookingCron::runJob($id_cron);
			});

			// Make sure the cron event hasn't been yet scheduled.
			// After its execution, wp_next_scheduled will return false and
			// we will be able to register it again.
			if (!wp_next_scheduled($hook))
			{
				// schedule event starting from the current time for every minute, by
				// launching the cron listener hook (3rd argument)
				wp_schedule_event(time(), $interval, $hook);
			}
		}
	}

	/**
	 * Executes the specified cron job.
	 *
	 * @param 	integer  $id_cron  The cron job ID.
	 *
	 * @return 	boolean  True on success, false otherwise.
	 */
	public static function runJob($id_cron)
	{
		// require the main library in case WPCron runs the job
		require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikbooking.php';

		$app = JFactory::getApplication();

		/**
		 * Initialize timezone handler when WP-Cron executes the job to make sure we
		 * set the proper timezone. This is just for the scheduled cron job execution.
		 * 
		 * @since 	1.6.3
		 */
		JDate::getDefaultTimezone();
		date_default_timezone_set($app->get('offset', 'UTC'));

		$error  = null;
		$status = false;

		// force the option in request to prevent weird behaviors
		$app->input->set('option', 'com_vikbooking');

		// get CRON JOB model
		$model = VBOMvcModel::getInstance('cronjob');

		try
		{
			// dispatch cron job
			$status = $model->dispatch($id_cron);

			if (!$status)
			{
				// get any registered error
				$error = $model->getError($last = null, $string = true);
			}
		}
		catch (Exception $e)
		{
			// catch any exception and go ahead
			$error = $e->getMessage();
		}
		catch (Throwable $e)
		{
			// catch any failure and go ahead
			$error = sprintf('%s in %s on line %d', $e->getMessage(), $e->getFile(), $e->getLine());
		}

		if (!$status && $error)
		{
			$item = $model->getItem($id_cron);

			if ($item)
			{
				// register error message within the logs of the cron job
				$item->logs = "<div style=\"color:var(--vbo-red-color);\">$error</div>\n<hr />\n" . $item->logs;

				$model->save($item);
			}
		}

		/**
		 * Restore standard timezone when WP-Cron executes the job to make sure we
		 * set the proper timezone. This is just for the scheduled cron job execution.
		 * 
		 * @since 	1.6.3
		 */
		date_default_timezone_set(JDate::getDefaultTimezone());

		return $status;
	}

	/**
	 * Returns a list of published cron jobs.
	 * 
	 * @param   boolean  True to return all the cron job columns.
	 * 
	 * @return 	array    An array of cron jobs.
	 */
	public static function getJobs($all = false)
	{
		$dbo = JFactory::getDbo();

		// retrieve all the published cron jobs
		$q = $dbo->getQuery(true)
			->from($dbo->qn('#__vikbooking_cronjobs'))
			->where($dbo->qn('published') . ' = 1');

		if ($all)
		{
			$q->select('*');
		}
		else
		{
			$q->select($dbo->qn('id'));
		}

		$dbo->setQuery($q);

		if ($all)
		{
			return $dbo->loadObjectList();
		}

		return $dbo->loadColumn();
	}

	/**
	 * Creates the HTML to support a field for the selection of the
	 * recurrence while creating/editing a cron job.
	 * 
	 * @param 	array 	$forms  An associative array containing the HTML.
	 * @param 	object 	$view   The view instance.
	 * 
	 * @return 	array 	$forms  The resulting HTML array.
	 */
	public static function addScheduleControlForm($forms, $view)
	{
		if (!is_array($forms))
		{
			$forms = [];
		}

		if (!isset($forms['cronjob']))
		{
			// init details section
			$forms['cronjob'] = '';
		}

		$schedule_key = null;

		// check whether the schedule key property exists
		if (!$view->row || !array_key_exists('schedule_key', $view->row))
		{
			static::installScheduleControl();
		}
		else
		{
			$schedule_key = $view->row['schedule_key'];
		}

		// fetch list of supported schedule intervals
		$schedules = wp_get_schedules();

		// sort the options in ascending order
		uasort($schedules, function($a, $b)
		{
			return $a['interval'] - $b['interval'];
		});

		$options = [];

		foreach ($schedules as $k => $schedule)
		{
			$options[] = JHtml::fetch('select.option', $k, $schedule['display']);
		}

		$options = JHtml::fetch('select.options', $options, 'value', 'text', $schedule_key ?: 'daily');

		$label = __('Recurrence', 'vikbooking');

		// build HTML control
		$forms['cronjob'] .=
<<<HTML
<div class="vbo-param-container">
	<div class="vbo-param-label">$label</div>
	<div class="vbo-param-setting"><select name="schedule_key">$options</select></div>
</div>
HTML
		;

		return $forms;
	}

	/**
	 * Executes while saving a cron job, so that it is possible to 
	 * inject within the array to save the selected recurrence.
	 * 
	 * @param 	boolean  $save  The saving flag.
	 * @param 	array 	 &$src  The array holding the data to save.
	 * 
	 * @return 	boolean  False to abort the saving process.
	 * 
	 */
	public static function saveScheduleControl($save, &$src)
	{
		if (is_null($save))
		{
			$save = true;
		}

		$src = (array) $src;

		$input = JFactory::getApplication()->input;

		// fetch schedule key from request
		$schedule_key = $input->get('schedule_key', null, 'string');

		// make sure the schedule key has been specified
		if (!is_null($schedule_key))
		{
			// inject schedule key into the array to save
			$src['schedule_key'] = $schedule_key;
		}

		return $save;
	}

	/**
	 * Checks the cron job scheduling after updating a record.
	 * 
	 * @param 	boolean  $status  Dummy argument for WordPress bc.
	 * @param 	array    $cron    The cron jobs details.
	 * @param 	boolean  $is_new  True in case of insert.
	 * 
	 * @return 	void
	 */
	public static function checkSchedulingAfterSave($status, $cron, $is_new)
	{
		if (!$is_new)
		{
			// check only in case of update
			static::checkJobScheduling($cron);
		}
	}

	/**
	 * Unschedules the cron jobs that are going to be deleted.
	 * 
	 * @param 	boolean  $delete  The deleting flag.
	 * @param 	object   $item    The item to delete.
	 * 
	 * @return 	boolean  False to abort the deleting process.
	 */
	public static function checkSchedulingBeforeDelete($delete, $item)
	{
		if (is_null($delete))
		{
			$delete = true;
		}

		if ($delete)
		{
			// turn off publishing to detach the scheduling
			$item->published = 0;

			// trigger changes
			static::checkJobScheduling($item);
		}

		return $delete;
	}

	/**
	 * Checks whether an existing schedule should be recreated as a result
	 * of any significant changes.
	 * 
	 * @param 	array  $cron  The cron record.
	 * 
	 * @return 	void
	 */
	protected static function checkJobScheduling($cron)
	{
		$cron = (array) $cron;

		if (!array_key_exists('published', $cron) || !array_key_exists('schedule_key', $cron))
		{
			// reload all the details of the updated cron job
			$cron = VBOMvcModel::getInstance('cronjob')->getItem(@$cron['id']);
		}
		else
		{
			// cast to object
			$cron = (object) $cron;
		}

		if (!$cron)
		{
			// ops...
			return;
		}

		if (!isset($cron->schedule_key))
		{
			// property not yet installed, use the default one to properly
			// unschedule the registered event
			$cron->schedule_key = 'daily';
		}

		// build cron listener hook
		$hook = static::getScheduleHook($cron);

		// fetch the next scheduled event
		$event = wp_get_scheduled_event($hook);

		if (!$event)
		{
			// no scheduled event, do nothing...
			return;
		}

		if (!$cron->published || $cron->schedule_key !== $event->schedule)
		{
			// unschedule the event
			wp_unschedule_event($event->timestamp, $hook);
		}
	}

	/**
	 * Returns the hook to be used while scheduling the execution
	 * of a cron job.
	 * 
	 * @param 	object 	$cron  The cron object.
	 * 
	 * @return 	string  The resulting hook.
	 */
	protected static function getScheduleHook($cron)
	{
		$cron = (object) $cron;

		// build cron listener hook
		return implode('_', [
			'vikbooking',
			'cron',
			preg_replace("/\.php$/", '', $cron->class_file),
			$cron->id,
		]);
	}

	/**
	 * Install the resources needed to support a custom recurrence for
	 * the created cron jobs.
	 * 
	 * @return 	void
	 */
	protected static function installScheduleControl()
	{
		$fields = VBOMvcModel::getInstance('cronjob')->getTableColumns();

		if (!isset($fields['schedule_key']))
		{
			$db = JFactory::getDbo();

			// alter the table to allow the storage of the selected recurrence
			$query = "ALTER TABLE `#__vikbooking_cronjobs` ADD COLUMN `schedule_key` varchar(64) DEFAULT NULL AFTER `published`";

			$db->setQuery($query);
			$db->execute();
		}
	}
}
