<?php
/**
 * Licensed under the GNU GPL license.
 * http://www.gnu.org/licenses/gpl.html
 */

if ( ! class_exists( 'ThemeUpdateChecker' ) ) :

	/**
	 * A custom theme update checker.
	 *
	 * @author Janis Elsts
	 * @copyright 2012
	 * @version 1.2
	 * @access public
	 */
	class ThemeUpdateChecker {
		public $theme                   = '';              //The theme associated with this update checker instance.
		public $metadataUrl             = '';        //The URL of the theme's metadata file.
		public $enableAutomaticChecking = true; //Enable/disable automatic update checks.

		protected $optionName          = '';      //Where to store update info.
		protected $automaticCheckDone  = false;
		protected static $filterPrefix = 'tuc_request_update_';

		/**
		 * Class constructor.
		 *
		 * @param string $theme Theme slug, e.g. "twentyten".
		 * @param string $metadataUrl The URL of the theme metadata file.
		 * @param boolean $enableAutomaticChecking Enable/disable automatic update checking. If set to FALSE, you'll need to explicitly call checkForUpdates() to, err, check for updates.
		 */
		public function __construct( $theme, $metadataUrl, $enableAutomaticChecking = true ) {
			$this->metadataUrl             = $metadataUrl;
			$this->enableAutomaticChecking = $enableAutomaticChecking;
			$this->theme                   = $theme;
			$this->optionName              = 'external_theme_updates-' . $this->theme;

			$this->installHooks();
		}

		/**
		 * Install the hooks required to run periodic update checks and inject update info
		 * into WP data structures.
		 *
		 * @return void
		 */
		public function installHooks() {
			//Check for updates when WordPress does. We can detect when that happens by tracking
			//updates to the "update_themes" transient, which only happen in wp_update_themes().
			if ( $this->enableAutomaticChecking ) {
				add_filter( 'pre_set_site_transient_update_themes', [ $this, 'onTransientUpdate' ] );
			}

			//Insert our update info into the update list maintained by WP.
			add_filter( 'site_transient_update_themes', [ $this, 'injectUpdate' ] );

			//Delete our update info when WP deletes its own.
			//This usually happens when a theme is installed, removed or upgraded.
			add_action( 'delete_site_transient_update_themes', [ $this, 'deleteStoredData' ] );
		}

		/**
		 * Retrieve update info from the configured metadata URL.
		 *
		 * Returns either an instance of ThemeUpdate, or NULL if there is
		 * no newer version available or if there's an error.
		 *
		 * @uses wp_remote_get()
		 *
		 * @param array $queryArgs Additional query arguments to append to the request. Optional.
		 * @return ThemeUpdate
		 */
		public function requestUpdate( $queryArgs = [] ) {
			//Query args to append to the URL. Themes can add their own by using a filter callback (see addQueryArgFilter()).
			$queryArgs['installed_version'] = $this->getInstalledVersion();
			$queryArgs                      = apply_filters( self::$filterPrefix . 'query_args-' . $this->theme, $queryArgs );

			//Various options for the wp_remote_get() call. Themes can filter these, too.
			$options = [
				'timeout' => 10, //seconds
			];
			$options = apply_filters( self::$filterPrefix . 'options-' . $this->theme, $options );

			$url = $this->metadataUrl;
			if ( ! empty( $queryArgs ) ) {
				$url = add_query_arg( $queryArgs, $url );
			}

			//Send the request.
			$result = wp_remote_get( $url, $options );

			//Try to parse the response
			$themeUpdate = null;
			$code        = wp_remote_retrieve_response_code( $result );
			$body        = wp_remote_retrieve_body( $result );
			if ( ( $code == 200 ) && ! empty( $body ) ) {
				$themeUpdate = ThemeUpdate::fromJson( $body );
				//The update should be newer than the currently installed version.
				if ( ( $themeUpdate != null ) && version_compare( $themeUpdate->version, $this->getInstalledVersion(), '<=' ) ) {
					$themeUpdate = null;
				}
			}

			$themeUpdate = apply_filters( self::$filterPrefix . 'result-' . $this->theme, $themeUpdate, $result );
			return $themeUpdate;
		}

		/**
		 * Get the currently installed version of our theme.
		 *
		 * @return string Version number.
		 */
		public function getInstalledVersion() {
			if ( function_exists( 'wp_get_theme' ) ) {
				$theme = wp_get_theme( $this->theme );
				return $theme->get( 'Version' );
			}

			/** @noinspection PhpDeprecationInspection wp_get_themes() used for compatibility with WP 3.3 and below. */
			foreach ( wp_get_themes() as $theme ) {
				if ( $theme['Stylesheet'] === $this->theme ) {
					return $theme['Version'];
				}
			}
			return '';
		}

		/**
		 * Check for theme updates.
		 *
		 * @return void
		 */
		public function checkForUpdates() {
			$state = get_option( $this->optionName );
			if ( empty( $state ) ) {
				$state                 = new StdClass();
				$state->lastCheck      = 0;
				$state->checkedVersion = '';
				$state->update         = null;
			}

			$state->lastCheck      = time();
			$state->checkedVersion = $this->getInstalledVersion();
			update_option( $this->optionName, $state ); //Save before checking in case something goes wrong

			$state->update = $this->requestUpdate();
			update_option( $this->optionName, $state );
		}

		/**
		 * Run the automatic update check, but no more than once per page load.
		 * This is a callback for WP hooks. Do not call it directly.
		 *
		 * @param mixed $value
		 * @return mixed
		 */
		public function onTransientUpdate( $value ) {
			if ( ! $this->automaticCheckDone ) {
				$this->checkForUpdates();
				$this->automaticCheckDone = true;
			}
			return $value;
		}

		/**
		 * Insert the latest update (if any) into the update list maintained by WP.
		 *
		 * @param StdClass $updates Update list.
		 * @return array Modified update list.
		 */
		public function injectUpdate( $updates ) {
			$state = get_option( $this->optionName );

			//Is there an update to insert?
			if ( ! empty( $state ) && isset( $state->update ) && ! empty( $state->update ) ) {
				$updates->response[ $this->theme ] = $state->update->toWpFormat();
			}

			return $updates;
		}

		/**
		 * Delete any stored book-keeping data.
		 *
		 * @return void
		 */
		public function deleteStoredData() {
			delete_option( $this->optionName );
		}

		/**
		 * Register a callback for filtering query arguments.
		 *
		 * The callback function should take one argument - an associative array of query arguments.
		 * It should return a modified array of query arguments.
		 *
		 * @param callable $callback
		 * @return void
		 */
		public function addQueryArgFilter( $callback ) {
			add_filter( self::$filterPrefix . 'query_args-' . $this->theme, $callback );
		}

		/**
		 * Register a callback for filtering arguments passed to wp_remote_get().
		 *
		 * The callback function should take one argument - an associative array of arguments -
		 * and return a modified array or arguments. See the WP documentation on wp_remote_get()
		 * for details on what arguments are available and how they work.
		 *
		 * @param callable $callback
		 * @return void
		 */
		public function addHttpRequestArgFilter( $callback ) {
			add_filter( self::$filterPrefix . 'options-' . $this->theme, $callback );
		}

		/**
		 * Register a callback for filtering the theme info retrieved from the external API.
		 *
		 * The callback function should take two arguments. If a theme update was retrieved
		 * successfully, the first argument passed will be an instance of  ThemeUpdate. Otherwise,
		 * it will be NULL. The second argument will be the corresponding return value of
		 * wp_remote_get (see WP docs for details).
		 *
		 * The callback function should return a new or modified instance of ThemeUpdate or NULL.
		 *
		 * @param callable $callback
		 * @return void
		 */
		public function addResultFilter( $callback ) {
			add_filter( self::$filterPrefix . 'result-' . $this->theme, $callback, 10, 2 );
		}
	}

endif;

if ( ! class_exists( 'ThemeUpdate' ) ) :

	/**
	 * A simple container class for holding information about an available update.
	 *
	 * @author Janis Elsts
	 * @copyright 2012
	 * @version 1.0
	 * @access public
	 */
	class ThemeUpdate {
		public $version;      //Version number.
		public $details_url;  //The URL where the user can learn more about this version.
		public $du; //The download URL for this version of the theme. Optional.

		/**
		 * Create a new instance of ThemeUpdate from its JSON-encoded representation.
		 *
		 * @param string $json Valid JSON string representing a theme information object.
		 * @return ThemeUpdate New instance of ThemeUpdate, or NULL on error.
		 */
		public static function fromJson( $json ) {
			$apiResponse = json_decode( $json );
			if ( empty( $apiResponse ) || ! is_object( $apiResponse ) ) {
				return null;
			}

			//Very, very basic validation.
			$valid = isset( $apiResponse->version ) && ! empty( $apiResponse->version ) && isset( $apiResponse->details_url ) && ! empty( $apiResponse->details_url );
			if ( ! $valid ) {
				return null;
			}

			$update = new self();
			foreach ( get_object_vars( $apiResponse ) as $key => $value ) {
				$update->$key = $value;
			}

			return $update;
		}

		/**
		 * Transform the update into the format expected by the WordPress core.
		 *
		 * @return array
		 */
		public function toWpFormat() {
			$update = [
				'new_version' => $this->version,
				'url'         => $this->details_url,
			];

			if ( ! empty( $this->du ) ) {
				$update['package'] = restly_decode_du( $this->du );
			}

			return $update;
		}
	}

endif;

$purchase_code_status = trim( get_option( 'restly_purchase_code_status' ) );
if ( $purchase_code_status == 'valid' ) {
	$update_checker = new ThemeUpdateChecker(
		'restly',
		'https://updatetheme.themepul.dev/restly/theme-info.json'
	);
}
