<?php
/**
 * Class Updater
 * @package AbsolutePluginsServices
 */
final class SE_License_SDK_Updater {

	/**
	 * Client
	 *
	 * @var SE_License_SDK_Client
	 */
	protected $client;

	/**
	 * Flag for checking if the init method is already called.
	 * @var bool
	 */
	private $did_init = false;

	/**
	 * AbsolutePluginsServices\License
	 *
	 * @var SE_License_SDK_License
	 */
	protected $license;

	/**
	 * Cache Key for current App
	 * @var string
	 */
	private $cache_key;

	/**
	 * Initialize the class
	 *
	 * @param SE_License_SDK_Client $client The Client.
	 * @param SE_License_SDK_License $license The license.
	 */
	public function __construct( SE_License_SDK_Client $client ) {
		$this->client    = $client;
		$this->cache_key = $this->client->getHookName( 'version_info' );
	}

	/**
	 * Initialize Updater
	 *
	 * @return void
	 */
	public function init() {
		if ( $this->did_init ) {
			return;
		}

		$fn = 'run_' . $this->client->getType() . '_hooks'; // run_(plugin/theme)_hooks

		// Run the hooks.
		if ( method_exists( $this, $fn ) ) {
			$this->$fn();
		}

		$this->did_init = true;
	}

	/**
	 * Set up WordPress filter hooks to get plugin update.
	 *
	 * @return void
	 */
	private function run_plugin_hooks() {
		add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_plugin_update' ] );
		add_filter( 'plugins_api', [ $this, 'plugins_api_filter' ], 10, 3 );

		register_activation_hook( $this->client->getPackageFile(), [ $this, 'delete_cached_version_info' ] );
		register_deactivation_hook( $this->client->getPackageFile(), [ $this, 'delete_cached_version_info' ] );
	}

	/**
	 * Set up WordPress filter hooks to get theme update.
	 *
	 * @return void
	 */
	private function run_theme_hooks() {
		add_filter( 'pre_set_site_transient_update_themes', [ $this, 'check_theme_update' ] );
		add_filter( 'themes_api', [ $this, 'themes_api_filter' ], 10, 3 );
		add_action( 'switch_theme', [ $this, 'delete_cached_version_info' ] );
	}

	/**
	 * Check for Update for this specific project
	 *
	 * @param object $transient_data plugin update transient data.
	 *
	 * @return object
	 */
	public function check_plugin_update( $transient_data ) {
		global $pagenow;

		if ( 'plugins.php' === $pagenow && is_multisite() ) {
			return $transient_data;
		}

		if ( ! is_object( $transient_data ) ) {
			$transient_data = new stdClass;
		}

		if ( ! empty( $transient_data->response ) && ! empty( $transient_data->response[ $this->client->getBasename() ] ) ) {
			return $transient_data;
		}

		$project_info = $this->get_information( 'plugin_update' );

		if ( false !== $project_info && is_object( $project_info ) && isset( $project_info->new_version ) ) {
			if ( version_compare( $this->client->getProjectVersion(), $project_info->new_version, '<' ) ) {
				unset( $project_info->sections );
				$transient_data->response[ $this->client->getBasename() ] = $project_info;
			}
			//$transient_data->last_checked = time();
			$transient_data->checked[ $this->client->getBasename() ] = $this->client->getProjectVersion();
		}

		return $transient_data;
	}

	/**
	 * Check theme update
	 *
	 * @param object $transient_data Theme update transient data.
	 *
	 * @return object
	 */
	public function check_theme_update( $transient_data ) {
		global $pagenow;

		if ( 'themes.php' === $pagenow && is_multisite() ) {
			return $transient_data;
		}

		if ( ! is_object( $transient_data ) ) {
			$transient_data = new stdClass();
		}

		if ( ! empty( $transient_data->response ) && ! empty( $transient_data->response[ $this->client->getSlug() ] ) ) {
			return $transient_data;
		}

		$project_info = $this->get_information( 'theme_update' );

		if ( false !== $project_info && is_object( $project_info ) && isset( $project_info->new_version ) ) {

			if ( version_compare( $this->client->getProjectVersion(), $project_info->new_version, '<' ) ) {
				$transient_data->response[ $this->client->getSlug() ] = (array) $project_info;
			}

			$transient_data->last_checked                        = time();
			$transient_data->checked[ $this->client->getSlug() ] = $this->client->getProjectVersion();
		}

		return $transient_data;
	}

	/**
	 * Get version info from database
	 *
	 * @return object|bool
	 */
	private function get_cached_version_info( $which ) {
		global $pagenow;


		// If updater page then fetch from API now
		if ( 'update-core.php' == $pagenow ) {
			return false; // Force fetching update
		}

		$info = get_transient( $this->cache_key . $which );

		if ( ! $info || ! isset( $info->name ) ) {
			return false; // Cache is expired.
		}

		return $this->__children_to_array( $info, [ 'icons', 'banners', 'sections' ] );
	}

	/**
	 * Set version info to database
	 *
	 * @param mixed $value data (version info) to cache.
	 *
	 * @return void
	 */
	private function set_cached_version_info( $value, $which ) {
		if ( ! $value ) {
			delete_transient( $this->cache_key . $which );
			return;
		}

		set_transient( $this->cache_key . $which, $value, 3 * HOUR_IN_SECONDS );
	}

	/**
	 * Delete cached version info
	 * @return void
	 */
	public function delete_cached_version_info() {
		delete_transient( $this->cache_key );
		$actions = [ 'plugin_update', 'plugin_information', 'theme_update', 'theme_information' ];

		foreach ( $actions as $which ) {
			delete_transient( $this->cache_key . $which );
		}
	}

	/**
	 * Get plugin info from WC API Manager
	 *
	 * @param string $action
	 *
	 * @return bool|array
	 */
	private function get_information( string $action, bool $force = false ) {

		// cache first.
		$project_info = $this->get_cached_version_info( $action );

		if ( false === $project_info || $force ) {
			$project_info = $this->get_updates( $action );

			$this->set_cached_version_info( $project_info, $action );
		}

		return $project_info;
	}

	private function get_updates( $action ) {
		// Updater doesn't need to care for license.
		// License key will be added to the request body by client (if available).
		// Server will provide update information without package/download link if license not available.
		// For free version response will contain the package/download link.

		// Update -> check-update,
		$response = $this->client->request( [
			'body'     => $this->client->get_admin_info(),
			'route'    => 'check-update',
			'blocking' => true
		] );

		if ( isset( $response['success'] ) && $response['success'] ) {
			$data = $response['data'];

			if ( 'plugin_update' !== $action ) {
				// information -> package-info
				$response = $this->client->request( [
					'body'     => $this->client->get_admin_info(),
					'route'    => 'package-info',
					'blocking' => true
				] );
				if ( isset( $response['success'] ) && $response['success'] ) {
					$data = array_merge( $data, $response['data'] );
				}
			}

			if ( isset( $data['product_id'] ) ) {
				unset( $data['product_id'] );
			}

			/**
			 * Filter API Response Data
			 *
			 * @param array $data
			 */
			$data = apply_filters( $this->client->getHookName( $action ), $data, $action );

			return $this->__children_to_array( (object) $data, [
				'icons',
				'banners',
				'sections',
				'compatibility',
				'ratings',
				'contributors',
				'screenshots',
				'tags'
			] );
		}

		return false;
	}

	/**
	 * Updates information on the "View version x.x details" page with custom data.
	 *
	 * @param mixed $data Plugin info Data.
	 * @param string $action Request Action.
	 * @param ?object $args API Args.
	 *
	 * @return object $data
	 */
	public function plugins_api_filter( $data, string $action = '', $args = null ) {
		if ( 'plugin_information' !== $action ) {
			return $data;
		}

		if ( ! isset( $args->slug ) || ( $args->slug !== $this->client->getSlug() ) ) {
			return $data;
		}

		return $this->get_information( 'plugin_information' );
	}

	public function themes_api_filter( $data, string $action = '', $args = null ) {
		if ( 'theme_information' !== $action ) {
			return $data;
		}

		if ( ! isset( $args->slug ) || ( $args->slug !== $this->client->getSlug() ) ) {
			return $data;
		}

		return $this->get_information( 'theme_information' );
	}

	/**
	 * Typecast child element to array or object
	 * Utility method
	 *
	 * @param array|stdClass $input the array or object to convert/typecast.
	 * @param array $children children array.
	 *
	 * @return array|object
	 */
	private function __children_to_array( $input, array $children = [] ) {
		if ( ! empty( $children ) && is_array( $children ) && ( is_object( $input ) || is_array( $input ) ) ) {
			$isObject = is_object( $input );
			foreach ( $children as $child ) {
				if ( call_user_func_array( $isObject ? 'property_exists' : 'array_key_exists', [ $input, $child ] ) ) {
					if ( $isObject ) {
						$input->{$child} = (array) $input->{$child};
					} else {
						$input[ $child ] = (array) $input->{$child};
					}
				}
			}
		}

		return $input;
	}

	final public function __clone() {
		trigger_error( 'Singleton. No cloning allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
	}

	/**
	 * Wakeup.
	 */
	final public function __wakeup() {
		trigger_error( 'Singleton. No serialization allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
	}
}

// End of file SE_License_SDK_Updater.php.
