<?php
declare(strict_types = 1);

/**
 * Méthodes de localisation.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class L10N
{
	/**
	 * Expression régulière du répertoire de langue.
	 *
	 * @var string
	 */
	const LANG_REGEXP = '`^[a-z]{2,3}(?:_[A-Z]{2,3})?$`';



	/**
	 * Texte localisé.
	 *
	 * @var array
	 */
	public static $text = [];



	/**
	 * Localisation de la date $date selon le format $format
	 * et le décalage horaire Auth::$tz.
	 *
	 * @param string $format
	 *   Deux formats de date sont acceptés :
	 *      - date (https://www.php.net/manual/fr/function.date.php)
	 *      - strftime (https://www.php.net/manual/fr/function.strftime.php)
	 * @param string $date
	 * @param bool $tz
	 *
	 * @return string
	 */
	public static function dt(string $format, string $date = 'now', bool $tz = TRUE): string
	{
		$dt = new DateTime($date);
		if ($tz)
		{
			$dt->setTimezone(new DateTimeZone(Auth::$tz));
		}

		// Équivalences strftime() - date().
		$format = strtr($format,
		[
			// Jour.
			'%a' => 'D', '%A' => 'l', '%d' => 'd', '%e' => 'j',
			'%j' => 'z', '%u' => 'N', '%w' => 'w',

			// Semaine.
			'%V' => 'W',

			// Mois.
			'%b' => 'M', '%B' => 'F', '%h' => 'M', '%m' => 'm',

			// Année.
			'%G' => 'o', '%g' => 'y', '%y' => 'y', '%Y' => 'Y',

			// Heure.
			'%H' => 'H', '%I' => 'h', '%l' => 'g', '%M' => 'i',
			'%p' => 'A', '%P' => 'a', '%S' => 's',

			// Fuseau horaire.
			'%z' => 'O', '%Z' => 'T',

			// Date.
			'%s' => 'U'
		]);

		// Localisation du nom des mois et des jours de semaine.
		$format = preg_replace_callback(
			'`[DlMF]`',
			function (array $m) use ($dt): string
			{
				return addcslashes(__($dt->format($m[0])), 'A..z');
			},
			$format
		);

		$dt = $dt->format($format);

		if (substr(Auth::$lang, 0, 2) == 'fr')
		{
			$dt = preg_replace('`(^|\s)0(\d\s+[a-z])`i', '$1$2', $dt);
			$dt = preg_replace('`((?:^|\s)1)(\s+[a-z])`i', '$1er$2', $dt);
		}

		return $dt;
	}

	/**
	 * Convertit un poids en octets en son multiple le plus approprié.
	 *
	 * @param mixed $size
	 *   Poids en octets.
	 * @param string $m
	 *   Force l'affichage dans le multiple indiqué (o, k, m, g).
	 *
	 * @return string
	 */
	public static function formatFilesize($size, string $m = ''): string
	{
		$size = (float) $size;

		$ko = 1024;
		$mo = 1024 * $ko;
		$go = 1024 * $mo;
		$to = 1024 * $go;

		if ((!$m && $size < $ko) || $m === 'o')
		{
			$size = ($size > 0)
				? sprintf(__('%s octets'), $size)
				: sprintf(__('%s octet'), $size);
		}
		else if ((!$m && $size < $mo) || $m === 'k')
		{
			$size = sprintf(__('%s Ko'), round($size/$ko, 2));
		}
		else if ((!$m && $size < $go) || $m === 'm')
		{
			$size = sprintf(__('%s Mo'), round($size/$mo, 2));
		}
		else if ((!$m && $size < $to) || $m === 'g')
		{
			$size = sprintf(__('%s Go'), round($size/$go, 2));
		}
		else
		{
			$size = sprintf(__('%s To'), round($size/$to, 2));
		}

		return self::numeric($size);
	}

	/**
	 * Formate un nombre.
	 *
	 * @param mixed $number
	 *
	 * @return string
	 */
	public static function formatNumber($number): string
	{
		return number_format((float) $number, 0, '', __(' '));
	}

	/**
	 * Formate une note.
	 *
	 * @param mixed $rating
	 *
	 * @return string
	 */
	public static function formatRating($rating): string
	{
		return number_format((float) $rating, 1, __(','), '');
	}

	/**
	 * Retourne n'importe quel nombre dans une notation courte.
	 *
	 * @param int $number
	 *
	 * @return string
	 */
	public static function formatShortNumber(int $number): string
	{
		if (!Config::$params['stats_short_numbers'])
		{
			return self::formatNumber($number);
		}

		$k = 1000;
		$m = $k * $k;
		$b = $k * $m;

		if ($number < 1000)
		{
			return "$number";
		}
		if ($number < $m)
		{
			return sprintf(__('%s k'), self::numeric(round($number / $k, 1)));
		}
		else if ($number < $b)
		{
			return sprintf(__('%s M'), self::numeric(round($number / $m, 2)));
		}
		else
		{
			return sprintf(__('%s Md'), self::numeric(round($number / $b, 2)));
		}
	}

	/**
	 * Retourne un texte localisé à partir d'un identifiant.
	 *
	 * @param string $id
	 *
	 * @return string
	 */
	public static function getText(string $id): string
	{
		switch ($id)
		{
			case 'incorrect_information' :
				return __('Informations incorrectes.');

			case 'message_maxlength' :
				return __('Votre message ne doit pas comporter plus de %s caractères.');

			case 'password_confirm_error' :
				return __('Les mots de passe ne correspondent pas.');

			case 'required_fields' :
				return __('Certains champs n\'ont pas été renseignés.');

			default :
				return '';
		}
	}

	/**
	 * Retourne la description par défaut des groupes prédéfinis.
	 *
	 * @param int $id
	 *   Identifiant du groupe.
	 * @param string $text
	 *   Description du groupe.
	 *
	 * @return string
	 */
	public static function getTextGroupDesc(int $id, string $text = ''): string
	{
		if ($text)
		{
			return $text;
		}
		switch ($id)
		{
			case 1 :
				return __('Ce groupe ne contient qu\'un seul utilisateur :'
					. ' la personne qui a installée la galerie, c\'est à dire'
					. ' l\'administrateur principal ou le webmaster.');

			case 2 :
				return __('Ce groupe ne contient aucun'
					. ' utilisateur enregistré. Il désigne l\'ensemble'
					. ' des visiteurs non membres de la galerie.');

			case 3 :
				return __('Ce groupe contient tout utilisateur'
					. ' qui vient de s\'enregistrer.');

			default :
				return '';
		}
	}

	/**
	 * Retourne le nom par défaut des groupes prédéfinis.
	 *
	 * @param int $id
	 *   Identifiant du groupe.
	 * @param string $text
	 *   Nom du groupe.
	 *
	 * @return string
	 */
	public static function getTextGroupName(int $id, string $text = ''): string
	{
		if ($text)
		{
			return $text;
		}
		switch ($id)
		{
			case 1 :
				return __('Super-administrateur');

			case 2 :
				return __('Invités');

			case 3 :
				return __('Nouveaux membres');

			default :
				return '';
		}
	}

	/**
	 * Retourne le titre par défaut des groupes prédéfinis.
	 *
	 * @param int $id
	 *   Identifiant du groupe.
	 * @param string $text
	 *   Titre du groupe.
	 *
	 * @return string
	 */
	public static function getTextGroupTitle(int $id, string $text = ''): string
	{
		if ($text)
		{
			return $text;
		}
		switch ($id)
		{
			case 1 :
				return __('Super-administrateur');

			case 2 :
				return __('Invité');

			case 3 :
				return __('Membre');

			default :
				return '';
		}
	}

	/**
	 * Retourne le texte correspondant à la cause
	 * du rejet de connexion d'un utilisateur.
	 *
	 * @param string $cause
	 *
	 * @return string
	 */
	public static function getTextLoginRejected(string $cause): string
	{
		switch ($cause)
		{
			case 'already_connected' :
				return __('Vous êtes déjà connecté.');

			case 'blacklist' :
				return __('Votre IP est sur liste noire.');

			case 'brute_force' :
				return __('Attaque par force brute détectée.');

			case 'data_format' :
				return __('Format des données incorrect.');

			case 'deactivated' :
				return __('Votre compte est désactivé.');

			case 'db_error' :
				return __('Une erreur s\'est produite.');

			case 'whitelist' :
				return __('Votre IP n\'est pas sur liste blanche.');

			default :
				return self::getText('incorrect_information');
		}
	}

	/**
	 * Retourne le texte correspondant à une page.
	 *
	 * @param string $page
	 *
	 * @return string
	 */
	public static function getTextPage(string $page): string
	{
		switch ($page)
		{
			case 'cameras' :
				return __('Appareils photos');

			case 'comments' :
				return __('Commentaires');

			case 'contact' :
				return __('Contact');

			case 'history' :
				return __('Historique');

			case 'members' :
				return __('Membres');

			case 'tags' :
				return __('Tags');

			case 'worldmap' :
				return __('Carte du monde');

			default :
				return $pages_params[$page]['title'] ?? '?';
		}
	}

	/**
	 * Retourne le texte correspondant à l'état d'un objet.
	 *
	 * @param int $status
	 *   Code correspondant à l'état.
	 *
	 * @return string
	 */
	public static function getTextStatus(int $status): string
	{
		switch ($status)
		{
			case -1 :
				return __('En attente');

			case 0 :
				return __('Désactivé');

			case 1 :
				return __('Activé');

			default :
				return '';
		}
	}

	/**
	 * Retourne le texte correspondant à l'abbréviation
	 * du genre d'un utilisateur.
	 *
	 * @param string $gender
	 *   Genre (abbrévation: 'F', 'M', '?').
	 *
	 * @return string
	 */
	public static function getTextUserGender(string $gender): string
	{
		switch ($gender)
		{
			case 'F' :
				return __('Femme');

			case 'M' :
				return __('Homme');

			default :
				return __('Inconnu');
		}
	}

	/**
	 * Retourne la liste de tous les fuseaux horaires disponibles,
	 * organisés par régions.
	 *
	 * @return array
	 */
	public static function getTzIdentifiers(): array
	{
		$get_identifiers = function(string $timezone, int $id) use (&$tz_identifiers)
		{
			$tz_identifiers[$timezone] = timezone_identifiers_list($id);
		};

		$list =
		[
			'Africa' => 1,
			'America' => 2,
			'Antarctica' => 4,
			'Arctic' => 8,
			'Asia' => 16,
			'Atlantic' => 32,
			'Australia' => 64,
			'Europe' => 128,
			'Indian' => 256,
			'Pacific' => 512
		];

		$tz_identifiers = [];
		foreach ($list as $timezone => $id)
		{
			$get_identifiers($timezone, $id);
		}

		return $tz_identifiers;
	}

	/**
	 * Installation automatique des langues
	 * disponibles dans le répertoire "locale".
	 *
	 * @return void
	 */
	public static function langInstall(): void
	{
		$locale_dir = GALLERY_ROOT . '/locale';
		$filemtime = filemtime($locale_dir);
		$params = Config::$params['lang_params'];
		if ($filemtime != $params['filemtime'])
		{
			$params['filemtime'] = $filemtime;
			$params['langs'] = [];
			foreach (scandir($locale_dir) as &$lang)
			{
				if (preg_match(self::LANG_REGEXP, $lang))
				{
					if (file_exists($file = $locale_dir . '/' . $lang . '/lang.php'))
					{
						include_once($file);
						if (isset($name))
						{
							$params['langs'][$lang] = $name;
							unset($name);
						}
					}
				}
			}
			ksort($params['langs']);
			Config::changeDBParams(['lang_params' => $params]);
		}
	}

	/**
	 * Définition des paramètres de localisation.
	 *
	 * @param string $lang
	 * @param string $tz
	 *
	 * @return void
	 */
	public static function locale(string $lang = '', string $tz = ''): void
	{
		$lang_params = Config::$params['lang_params']['langs'];

		// Paramètre de langue fourni.
		if (preg_match(self::LANG_REGEXP, $lang) && isset($lang_params[$lang]))
		{
			Auth::$lang = $lang;

			// On ajoute le code de la langue dans le cookie des préférences.
			if (!Auth::$connected && Config::$params['lang_switch'])
			{
				Auth::$prefs->add('lang', Auth::$lang);
			}
		}

		// Fuseau horaire fourni.
		if ($tz && in_array($tz, timezone_identifiers_list()))
		{
			Auth::$tz = $tz;
		}

		// Détection de la langue du client.
		if (!Auth::$lang && Config::$params['lang_detect']
		&& isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
		{
			preg_match_all(
				'`([a-z]{1,2}(?:-[a-z]{1,2})?)\s*(?:;\s*q\s*=\s*(1|0\.[0-9]+))?`i',
				$_SERVER['HTTP_ACCEPT_LANGUAGE'],
				$lang_parse
			);
			if (count($lang_parse[1]))
			{
				$langs = array_map(
					function($q) { return $q === '' ? 1 : $q; },
					array_combine($lang_parse[1], $lang_parse[2])
				);
				arsort($langs, SORT_NUMERIC);
				foreach ($langs as $lang => &$val)
				{
					$lang = strtolower($lang);
					foreach ($lang_params as $code => &$name)
					{
						if ($lang == $code)
						{
							Auth::$lang = $code;
							break 2;
						}
					}
				}
			}
		}

		// Langue par défaut.
		if (!Auth::$lang)
		{
			Auth::$lang = Config::$params['lang_default'];
		}
		if (!preg_match(self::LANG_REGEXP, Auth::$lang))
		{
			Auth::$lang = 'en';
		}

		// Fuseau horaire par défaut.
		if (!Auth::$tz)
		{
			Auth::$tz = Config::$params['tz_default'];
		}

		// Chargement du fichier de langue.
		if (file_exists($file = GALLERY_ROOT . '/locale/' . Auth::$lang . '/text.php'))
		{
			include_once($file);
			self::$text =& $text;
		}

		// Chargement du fichier de personnalisation.
		if (file_exists($file = GALLERY_ROOT . '/locale/' . Auth::$lang . '/custom.php'))
		{
			include_once($file);
			if (isset($custom) && is_array($custom))
			{
				self::$text = array_merge(self::$text, $custom);
			}
		}
	}

	/**
	 * Localisation du caractère de séparation décimale.
	 * Exemple : 4.2 => 4,2
	 *
	 * @param float|string $float
	 *   Chaîne ou flottant à localiser.
	 *
	 * @return string
	 */
	public static function numeric($float): string
	{
		return preg_replace('`^(.*\d+)\D(\d+.*)$`', '$1' . __(',') . '$2', (string) $float);
	}
}
?>