<?php
class PMC_Cron_Store extends WP_Cron_Store {
	/**
	 * Status enum for "complete" state
	 */
	const STATUS_COMPLETE = 'complete';

	/**
	 * Status enum for "processing" state.  For future use.
	 */
	const STATUS_PROCESSING = 'processing';

	/**
	 * Status enum for "pending" state
	 */
	const STATUS_PENDING = 'pending';

	/**
	 * Array of pending cron jobs for time() + 10 minutes
	 */
	protected $_cron_array = array();

	/**
	 * Internal cache of next scheduled events, since we're no longer using the
	 * options table and WP object cache
	 */
	protected $_next_scheduled_events = array();

	/**
	 * Constructor sets up $this->_cron_array
	 *
	 * @since ?
	 */
	public function __construct() {
		// Set up the cron array
		$this->_setup_cron_array();
	}

	/**
	 * Delete any completed cron jobs older than one day on shutdown
	 *
	 * @since ?
	 */
	public function __destruct() {
		global $wpdb;

		$wpdb->query("DELETE FROM `" . $wpdb->prefix . "cron` WHERE `status`='" . self::STATUS_COMPLETE . "' AND `timestamp`<" . strtotime('-1 day'));

	}

	/**
	 * Marks the event as completed and removes it from the current instance of
	 * $this->_cron_array
	 *
	 * @since ?
	 *
	 * @param int $timestamp
	 * @param string $hook
	 * @param array $args
	 */
	public function complete_event( $timestamp, $hook, $args ) {
		global $wpdb;

		$args = serialize($args);
		$key = md5($args);
		$id = $this->_get_cron_id( $timestamp, $hook, $args, $key );

		unset( $this->_cron_array[$timestamp][$hook][$key] );

		if ( empty($this->_cron_array[$timestamp][$hook]) )
			unset( $this->_cron_array[$timestamp][$hook] );

		if ( empty($this->_cron_array[$timestamp]) )
			unset( $this->_cron_array[$timestamp] );

		$wpdb->query("UPDATE `" . $wpdb->prefix . "cron` SET `status`='" . self::STATUS_COMPLETE . "' WHERE `id`='" . $id . "' LIMIT 1");
	}

	/**
	 * Searches for a scheduled event
	 *
	 * @since ?
	 *
	 * @param string $hook
	 * @param int $timestamp
	 * @param array $args
	 *
	 * @return obj|false $event object or false if it doesn't exist
	 */
	public function get_event( $hook, $timestamp, $args = array() ) {
		global $wpdb;

		$key = md5(serialize($args));

		// Check if the event exists in the current cron array
		if ( isset($this->_cron_array[$timestamp][$hook][$key]) ) {
			return (object) array(
				'hook' => $hook,
				'args' => $args,
				'timestamp' => $timestamp,
				'schedule' => $this->_cron_array[$timestamp][$hook][$key]['schedule'],
				'interval' => $this->_cron_array[$timestamp][$hook][$key]['interval']
			);
		}

		// If the timestamp is empty or "next", see if we have it in the current cron array
		if ( empty($timestamp) || 'next' == $timestamp ) {
			if ( isset($this->_next_scheduled_events[$hook][$key]) )
				return $this->_next_scheduled_events[$hook][$key];

			foreach ( $this->_cron_array as $cron_timestamp => $cron ) {
				if ( isset( $cron[$hook][$key] ) ) {
					$this->_next_scheduled_events[$hook][$key] = (object) array(
						'hook' => $hook,
						'args' => $args,
						'timestamp' => $cron_timestamp,
						'schedule' => $cron[$hook][$key]['schedule'],
						'interval' => $cron[$hook][$key]['interval']
					);

					return $this->_next_scheduled_events[$hook][$key];
				}
			}

		}

		// Query the event from the database
		$event = $wpdb->get_row("SELECT `hook`, `args`, `timestamp`, `schedule`, `interval` FROM `" . $wpdb->prefix . "cron` WHERE `hook`='" . $hook . "' AND `key`='" . $key . "' AND `status`!='" . self::STATUS_COMPLETE . "' ORDER BY `timestamp` ASC LIMIT 1");

		if ( $event ) {
			$event->args = unserialize($event->args);

			// Add the event to $this->_next_scheduled_events so we won't have to query it next time
			if ( empty($timestamp) || 'next' == $timestamp )
				$this->_next_scheduled_events[$hook][$key] = $event;

			return $event;
		}

		return false;
	}

	/**
	 * Returns an array of all pending jobs within the next 10 minutes
	 *
	 * @since ?
	 *
	 * @param array $args Unused
	 *
	 * @return array
	 */
	public function get_events( $args = array() ) {
		return $this->_get_cron_array();
	}

	/**
	 * Inserts a new event
	 *
	 * @since ?
	 *
	 * @param int $timestamp
	 * @param string $hook
	 * @param array $args
	 * @param null|string $recurrence
	 * @param null|int $interval
	 */
	public function insert_event( $timestamp, $hook, $args, $recurrence = null, $interval = null ) {
		$key = md5(serialize($args));

		$this->_cron_array[$timestamp][$hook][$key] = array(
			'schedule' => $recurrence,
			'args' => $args,
			'interval' => $interval
		);
		uksort( $this->_cron_array, 'strnatcasecmp' );
		$this->_set_cron_array();
	}

	/**
	 * Retrieve cron info array option.
	 *
	 * @since 2.1.0
	 *
	 * @return array CRON info array.
	 */
	protected function _get_cron_array()  {
		return $this->_setup_cron_array();
	}

	/**
	 * Sets up the cron array with data.
	 *
	 * @since ?
	 *
	 * @param array $crons Optional, default WP_Cron_Store passes this for legacy reasons
	 *
	 * @return array $this->_cron_array
	 */
	protected function _setup_cron_array( $crons = array() ) {
		global $wpdb;

		if ( !empty($this->_cron_array) ) {
			return $this->_cron_array;
		}

		$old_crons = get_option('cron', array());
		if ( !empty($old_crons) )
			$this->_upgrade_cron_array($old_crons);

		// Get jobs that need to run + jobs for 10 minutes in the future (for wp_schedule_single_event())
		$crons = $wpdb->get_results("SELECT `key`, `hook`, `timestamp`, `schedule`, `args`, `interval` FROM `" . $wpdb->prefix . "cron` WHERE `status`='" . self::STATUS_PENDING . "' AND `timestamp`<=" . strtotime('+10 minutes') . " ORDER BY `timestamp` ASC");
		if ( $crons ) {
			foreach ( $crons as $cron ) {
				$this->_cron_array[$cron->timestamp][$cron->hook][$cron->key] = array(
					'schedule' => $cron->schedule,
					'args' => unserialize($cron->args),
					'interval' => $cron->interval
				);
			}
		}

		return $this->_cron_array;
	}

	/**
	 * Updates the CRON option with the new CRON array.
	 *
	 * @since 2.1.0
	 *
	 * @param array $crons Cron info array from {@link _get_cron_array()}.
	 */
	protected function _set_cron_array( $crons = array() ) {
		global $wpdb;

		if ( empty($crons) ) {
			$crons = $this->_cron_array;
		}

		$crons['version'] = 3;

		foreach ( $crons as $timestamp => $hooks ) {
			if ( 'version' === $timestamp ) {
				continue;
			}
			foreach ( $hooks as $hook => $args ) {
				foreach ( $args as $key => $params ) {
					// @todo use wpdb prepare, insert
					$id = $this->_get_cron_id( $timestamp, $hook, $params['args'] );

					$result = $wpdb->query("INSERT INTO `" . $wpdb->prefix . "cron` SET `id`='" . $id . "', `key`='" . $key . "', `hook`='" . $hook . "', `timestamp`='" . $timestamp . "', `schedule`='" . $params['schedule'] . "', `args`='" . serialize($params['args']) . "', `interval`='" . $params['interval'] . "'");

					if ( !$result ) {
						error_log($wpdb->last_error);
					}
				}
			}
		}
	}

	/**
	 * Generates an id for storing the cron in the database
	 *
	 * This method is private because nothing should override how the cron id's
	 * are generated
	 *
	 * @since ?
	 *
	 * @param int $timestamp
	 * @param string $hook
	 * @param array $args
	 * @param null|string $key Key made from md5 hash of serialized $args array
	 *
	 * @return string
	 */
	private function _get_cron_id( $timestamp, $hook, $args = array(), $key = null ) {
		$args = (is_array($args)) ? serialize($args) : $args;
		$key = (is_null($key)) ? md5($args) : $key;
		return md5($key . $timestamp . $hook . $args);
	}

	/**
	 * Upgrade a Cron info array.
	 *
	 * This function upgrades the Cron info array to version 3.
	 *
	 * @since 2.1.0
	 *
	 * @param array $cron Cron info array from {@link _get_cron_array()}.
	 * @return array An upgraded Cron info array.
	 */
	protected function _upgrade_cron_array($crons) {
		global $wpdb;

		if ( isset($crons['version']) && 3 == $crons['version'])
			return $crons;

		unset($crons['version']);
		if ( $wpdb->get_var("SHOW TABLES LIKE '" . $wpdb->prefix . "cron'") != $wpdb->prefix . 'cron' ) {
			$wpdb->query("CREATE TABLE `" . $wpdb->prefix . "cron` (
				  `id` char(32) NOT NULL,
				  `key` char(32) NOT NULL,
				  `hook` varchar(255) NOT NULL,
				  `timestamp` int(11) NOT NULL,
				  `schedule` varchar(32) NOT NULL,
				  `args` text NOT NULL,
				  `interval` int(11) NOT NULL,
				  `status` enum('complete','processing','pending') NOT NULL DEFAULT 'pending',
				  `last_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
				  PRIMARY KEY (`id`),
				  KEY `timestamp` (`timestamp`),
				  KEY `status` (`status`),
				  KEY `hook` (`hook`),
				  KEY `key` (`key`)
				) DEFAULT CHARSET=utf8;");
		}

		foreach ( (array) $crons as $timestamp => $hooks) {
			foreach ( (array) $hooks as $hook => $args ) {
			    $args = array_pop($args);
				$key = md5(serialize($args['args']));
				$this->_cron_array[$timestamp][$hook][$key] = $args;
			}
		}

		$this->_cron_array['version'] = 3;
		delete_option('cron');
	}
}


if ( !isset( $wp_cron_store ) )
	$wp_cron_store = new PMC_Cron_Store;
