<?php
/**
 * Main Traduttore Registry library.
 *
 * @since 1.0.0
 *
 * Copyright (c) 2018-2020 required (email: info@required.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2 or, at
 * your discretion, any later version, as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

namespace Required\Traduttore_Registry;

use DateTime;

const TRANSIENT_KEY_PLUGIN = 'traduttore-registry-plugins';
const TRANSIENT_KEY_THEME  = 'traduttore-registry-themes';

/**
 * Adds a new project to load translations for.
 *
 * @since 1.0.0
 *
 * @param string $type    Project type. Either plugin or theme.
 * @param string $slug    Project directory slug.
 * @param string $api_url Full GlotPress API URL for the project.
 */
function add_project( $type, $slug, $api_url ) {
	if ( ! has_action( 'init', __NAMESPACE__ . '\register_clean_translations_cache' ) ) {
		add_action( 'init', __NAMESPACE__ . '\register_clean_translations_cache', 9999 );
	}

	/**
	 * Short-circuits translations API requests for private projects.
	 */
	add_filter(
		'translations_api',
		static function ( $result, $requested_type, $args ) use ( $type, $slug, $api_url ) {
			if ( $type . 's' === $requested_type && $slug === $args['slug'] ) {
				return get_translations( $type, $args['slug'], $api_url );
			}

			return $result;
		},
		10,
		3
	);

	/**
	 * Filters the translations transients to include the private plugin or theme.
	 *
	 * @see wp_get_translation_updates()
	 */
	add_filter(
		'site_transient_update_' . $type . 's',
		static function ( $value ) use ( $type, $slug, $api_url ) {
			if ( ! $value ) {
				$value = new \stdClass();
			}

			if ( ! isset( $value->translations ) ) {
				$value->translations = [];
			}

			$translations = get_translations( $type, $slug, $api_url );

			if ( ! isset( $translations['translations'] ) ) {
				return $value;
			}

			$installed_translations = get_installed_translations( $type );
			$locales                = get_available_locales();

			/** This filter is documented in wp-includes/update.php */
			$locales        = apply_filters( $type . 's_update_check_locales', $locales );
			$active_locales = array_unique( $locales );

			foreach ( (array) $translations['translations'] as $translation ) {
				if ( ! \in_array( $translation['language'], $active_locales, true ) ) {
					continue;
				}

				if ( $translation['updated'] && isset( $installed_translations[ $slug ][ $translation['language'] ] ) ) {
					$local  = sanitize_date( $installed_translations[ $slug ][ $translation['language'] ]['PO-Revision-Date'] );
					$remote = new DateTime( $translation['updated'] );

					if ( $local >= $remote ) {
						continue;
					}
				}

				$translation['type'] = $type;
				$translation['slug'] = $slug;

				$value->translations[] = $translation;
			}

			return $value;
		}
	);
}

/**
 * Registers actions for clearing translation caches.
 *
 * @since 1.1.0
 */
function register_clean_translations_cache() {
	$clear_plugin_translations = static function() {
		clean_translations_cache( 'plugin' );
	};
	$clear_theme_translations  = static function() {
		clean_translations_cache( 'theme' );
	};

	add_action( 'set_site_transient_update_plugins', $clear_plugin_translations );
	add_action( 'delete_site_transient_update_plugins', $clear_plugin_translations );

	add_action( 'set_site_transient_update_themes', $clear_theme_translations );
	add_action( 'delete_site_transient_update_themes', $clear_theme_translations );
}

/**
 * Clears existing translation cache for a given type.
 *
 * @since 1.1.0
 *
 * @param string $type Project type. Either plugin or theme.
 */
function clean_translations_cache( $type ) {
	$transient_key = constant( __NAMESPACE__ . '\TRANSIENT_KEY_' . strtoupper( $type ) );
	$translations  = get_site_transient( $transient_key );

	if ( ! \is_object( $translations ) ) {
		return;
	}

	/*
	 * Don't delete the cache if the transient gets changed multiple times
	 * during a single request. Set cache lifetime to maximum 15 seconds.
	 */
	$cache_lifespan   = 15;
	$time_not_changed = isset( $translations->_last_checked ) && ( time() - $translations->_last_checked ) > $cache_lifespan;

	if ( ! $time_not_changed ) {
		return;
	}

	delete_site_transient( $transient_key );
}

/**
 * Gets the translations for a given project.
 *
 * @since 1.0.0
 *
 * @param string $type Project type. Either plugin or theme.
 * @param string $slug Project directory slug.
 * @param string $url  Full GlotPress API URL for the project.
 * @return array Translation data.
 */
function get_translations( $type, $slug, $url ) {
	$transient_key = constant( __NAMESPACE__ . '\TRANSIENT_KEY_' . strtoupper( $type ) );
	$translations  = get_site_transient( $transient_key );

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

	if ( isset( $translations->{$slug} ) && \is_array( $translations->{$slug} ) ) {
		return $translations->{$slug};
	}

	$result = json_decode( wp_remote_retrieve_body( wp_remote_get( $url, [ 'timeout' => 2 ] ) ), true );
	if ( ! \is_array( $result ) ) {
		// Cache an empty result in case of a failure
		// and retry on next update check.
		$result = [];
	}

	$translations->{$slug}       = $result;
	$translations->_last_checked = time();

	set_site_transient( $transient_key, $translations );

	return $result;
}

/**
 * Sanitizes a date string.
 *
 * DateTime fails to parse date strings that contain brackets, such as
 * “Tue Dec 22 2015 12:52:19 GMT+0100 (West-Europa)”, which appears in
 * PO-Revision-Date headers. Sanitization ensures such date headers are
 * parsed correctly into DateTime instances.
 *
 * @since 2.1.0
 *
 * @param string $date_string The date string to sanitize.
 * @return \DateTime Date from string if parsable, otherwise the Unix epoch.
 */
function sanitize_date( $date_string ) {
	$date_and_timezone = explode( '(', $date_string );
	$date_no_timezone  = trim( $date_and_timezone[0] );

	try {
		$date = new DateTime( $date_no_timezone );
	} catch ( \Exception $e ) {
		return new DateTime( '1970-01-01' );
	}

	return $date;
}

/**
 * Gets the installed translations.
 *
 * Results are cached.
 *
 * @since 2.2.0
 *
 * @see wp_get_installed_translations()
 *
 * @param string $type Project type. Either plugin or theme.
 * @return array Translation data.
 */
function get_installed_translations( $type ) {
	static $cache = [];

	if ( ! isset( $cache[ $type ] ) ) {
		$cache[ $type ] = wp_get_installed_translations( $type . 's' );
	}

	return $cache[ $type ];
}

/**
 * Gets all available and installed locales.
 *
 * Results are cached.
 *
 * @since 2.2.0
 *
 * @see get_available_languages()
 *
 * @return array List of installed locales.
 */
function get_available_locales() {
	static $cache = null;

	if ( ! isset( $cache ) ) {
		$cache = array_values( get_available_languages() );
	}

	return $cache;
}
