<?php
declare(strict_types = 1);

/**
 * Méthodes utilitaires de traitement de chaînes, de nombres et de tableaux.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Utility
{
	/**
	 * Méthode de tri par ordre alphabétique croissant
	 * pour fonctions de tri PHP nécessitant l'utilisation
	 * d'une fonction utilisateur (uasort, uksort, usort).
	 *
	 * @param string $a
	 * @param string $b
	 *
	 * @return int
	 */
	public static function alphaSort(string $a, string $b): int
	{
		return strcasecmp(self::removeAccents($a), self::removeAccents($b));
	}

	/**
	 * Renomme une clé dans un tableau tout en préservant sa position.
	 *
	 * @param string $key_name
	 * @param string $key_newname
	 * @param array $array
	 * @param bool $recursive
	 *
	 * @return array
	 */
	public static function arrayKeyRename(string $key_name, string $key_newname,
	array &$array, bool $recursive = FALSE): array
	{
		if (array_key_exists($key_name, $array))
		{
			$position = array_search($key_name, array_keys($array));
			$array_insert = [$key_newname => $array[$key_name]];
			$array = array_merge(array_splice($array, 0, $position), $array_insert, $array);
			unset($array[$key_name]);
		}
		if ($recursive)
		{
			foreach ($array as $key => &$value)
			{
				if (is_array($value))
				{
					self::{__FUNCTION__}($key_name, $key_newname, $value, $recursive);
				}
			}
		}

		return $array;
	}

	/**
	 * Retire tous les objets se trouvant dans un tableau.
	 *
	 * @param array $array
	 *
	 * @return void
	 */
	public static function arrayRemoveObject(array &$array): void
	{
		array_walk_recursive($array, function(&$v)
		{
			if (is_object($v))
			{
				$v = NULL;
			}
		});
	}

	/**
	 * Vérifie la valeur d'une variable $_POST.
	 *
	 * @param string $name
	 * @param string $regexp
	 *
	 * @return bool
	 */
	public static function checkPost(string $name, string $regexp): bool
	{
		if (!isset($_POST[$name]) || is_array($_POST[$name]))
		{
			return FALSE;
		}

		return (bool) preg_match($regexp, $_POST[$name] = (string) $_POST[$name]);
	}

	/**
	 * Convertit une expression régulière du format POSIX au format PCRE.
	 *
	 * @param string $pattern
	 *
	 * @return string
	 */
	public static function convertPOSIXtoPCRE(string $pattern)
	{
		return strtr($pattern,
		[
			'[^[:alnum:]]' => '[^a-z0-9]',
			'[^[:alpha:]]' => '[^a-z]',
			'[^[:digit:]]' => '\D',
			'[^[:space:]]' => '\S',
			'[[:alnum:]]' => '[a-z0-9]',
			'[[:alpha:]]' => '[a-z]',
			'[[:digit:]]' => '\d',
			'[[:space:]]' => '\s',
			'[[:<:]]' => '\b',
			'[[:>:]]' => '\b'
		]);
	}

	/**
	 * Supprime les caractères "invisible" d'une chaîne de caractères,
	 * c'est à dire tous les caractères de contrôle sauf :
	 *    - Tabulation horizontale (09).
	 *    - Saut de ligne (0A ou \n).
	 *    - Retour chariot (0D ou \r).
	 * Ne sont pas non plus supprimés :
	 *    - Espace (20).
	 *    - Espace insécable (A0).
	 *
	 * @param string $str
	 *
	 * @return string
	 */
	public static function deleteInvisibleChars(string $str): string
	{
		$str_replace = preg_replace(
			'`[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{0080}-\x{009F}]+`u', '', $str
		);

		return is_string($str_replace) ? $str_replace : $str;
	}

	/**
	 * Retourne la regexp pour les adresses de courriel.
	 *
	 * @param int $limit
	 *   Longueur maximale de l'adresse.
	 *
	 * @return string
	 */
	public static function emailRegexp(int $limit = 255): string
	{
		return '(?!.{' . ($limit + 1) . '})'
			. '(?:[-a-z\d!#$%&\'*+/=?^_\`{|}~]+\.?)+(?<!\.)(?<!.{65})@(?!.{256})' 
			. self::URLRegexp('domain') . self::URLRegexp('tld');
	}

	/**
	 * Convertit n'importe quelle valeur en entier
	 * et la retourne sous forme d'une chaîne de caractères.
	 * Équivalent proche de la fonction PHP intval() mais non limitant
	 * par rapport à la valeur entière maximale du système.
	 * Voir : http://www.php.net/manual/fr/function.intval.php
	 *
	 * @param mixed $val
	 *
	 * @return string
	 */
	public static function getIntVal($val): string
	{
		// On ne s'occupe que des types numérique et chaîne de caractères.
		if (!is_numeric($val) && !is_string($val))
		{
			return (string) intval($val);
		}

		// Convertit le type en chaîne de caractères
		// et supprime les espaces de début et de fin.
		$val = trim((string) $val);

		// Convertit la notation scientifique en notation classique.
		if (preg_match('`^([-+])?(\d+)(?:\.(\d+))?E([-+])?(\d+)$`i', $val, $m))
		{
			$l = $m[5] - strlen($m[3]);
			$val = ($m[4] == '+')
				? ($l > 0
					? $m[1] . $m[2] . $m[3] . str_repeat('0', $l)
					: $m[1] . substr($m[2] . $m[3], 0, $l))
				: ($m[5] == '0' ? $val : '0');
		}

		// On retourne un nombre entier.
		return preg_match('`^(?:\+|(\-))?0*([1-9]\d*).*$`', $val, $m) ? $m[1] . $m[2] : '0';
	}

	/**
	 * Retourne le poids maximum des fichiers autorisé
	 * par le serveur en upload (en octets), ou 0 si aucune limite.
	 *
	 * @param string $method
	 *   Méthode d'envoi de fichier : 'files' ou 'post'.
	 *
	 * @return int
	 */
	public static function getUploadMaxFilesize(string $method = 'files'): int
	{
		$ini_get = function(string $directive): int
		{
			$val = strtolower(ini_get($directive));
			if (substr($val, 0, 1) == '-' || $val == '0')
			{
				return 0;
			}
			switch (substr($val, -1))
			{
				case 'g' : $f = 1024 * 1024 * 1024; break;
				case 'm' : $f = 1024 * 1024; break;
				case 'k' : $f = 1024; break;
				default  : $f = 1;
			}
			return (int) self::getIntVal(((float) $val) * $f);
		};

		$limits = [];
		if ($val = $ini_get('memory_limit'))
		{
			$limits[] = $val;
		}
		if ($val = $ini_get('post_max_size'))
		{
			$limits[] = $val;
		}
		if ($method == 'files' && $val = $ini_get('upload_max_filesize'))
		{
			$limits[] = $val;
		}
		if (!$limits)
		{
			return 0;
		}

		return min($limits);
	}

	/**
	 * Détermine si une chaîne est vide, c'est à dire si
	 * elle ne contient aucun caractère ou aucun caractère
	 * autre que des caractères de contrôle et des espaces.
	 *
	 * @param string $str
	 *
	 * @return bool
	 */
	public static function isEmpty(string $str): bool
	{
		return (bool) preg_match('`^[\x{0000}-\x{0020}\x{007F}-\x{00A0}]*$`u', $str);
	}

	/**
	 * Détermine si une chaîne ressemble a un tableau représenté en JSON.
	 *
	 * @param mixed $str
	 *
	 * @return bool
	 */
	public static function isJson($str): bool
	{
		json_decode((string) $str, TRUE);

		return json_last_error() === JSON_ERROR_NONE;
	}

	/**
	 * Détermine si une chaîne est encodée en UTF-8.
	 *
	 * @param string $str
	 *
	 * @return bool
	 */
	public static function isUTF8(string $str): bool
	{
		// Source : http://www.w3.org/International/questions/qa-forms-utf-8.en.php
		// Ajout obligatoire d'un quantificateur "+" sur la première ligne,
		// sinon plantage sur certaines chaînes (bug PHP ?).
		return (bool) preg_match('`^(?:
			  [\x09\x0A\x0D\x20-\x7E]+			# ASCII
			| [\xC2-\xDF][\x80-\xBF]			# non-overlong 2-byte
			| \xE0[\xA0-\xBF][\x80-\xBF]		# excluding overlongs
			| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}	# straight 3-byte
			| \xED[\x80-\x9F][\x80-\xBF]		# excluding surrogates
			| \xF0[\x90-\xBF][\x80-\xBF]{2}		# planes 1-3
			| [\xF1-\xF3][\x80-\xBF]{3}			# planes 4-15
			| \xF4[\x80-\x8F][\x80-\xBF]{2}		# plane 16
		)*$`xs', $str);
	}

	/**
	 * Décode un tableau représenté en JSON.
	 *
	 * @param mixed $str
	 *
	 * @return mixed
	 */
	public static function jsonDecode($str)
	{
		if (!is_string($str))
		{
			return $str;
		}

		$array = json_decode($str, TRUE);

		return json_last_error() === JSON_ERROR_NONE ? $array : $str;
	}

	/**
	 * Encode un tableau en représentation JSON.
	 *
	 * @param array $array
	 *
	 * @return string
	 */
	public static function jsonEncode(array $array): string
	{
		return json_encode($array, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE);
	}

	/**
	 * Convertit les formats de sauts de ligne
	 * Windows (\r\n) et Mac (\r) en format Unix (\n).
	 *
	 * @param string $str
	 *
	 * @return string
	 */
	public static function LF(string $str): string
	{
		return preg_replace('`\r\n?`', "\n", $str);
	}

	/**
	 * Effectue une recherche dans une chaîne
	 * à partir d'une liste de mots-clés.
	 *
	 * @param string $subject
	 *   Chaîne dans laquelle effectuer la recherche.
	 * @param array|string $list
	 *   Liste de mots-clés à chercher.
	 * @param bool $word
	 *   Si TRUE, effectue la recherche dans l'un des mots de $subject,
	 *   sinon, effectue la recherche sur la totalité de la chaîne.
	 *
	 * @return array
	 *   Si l'un des mots-clés de la liste a été trouvé dans $subject,
	 *   alors retourne un tableau contenant ce mot-clé ('entry')
	 *   ainsi que la partie de la chaîne qui a matché ('match'),
	 *   sinon retourne un tableau vide.
	 */
	public static function listSearch(string $subject, $list, bool $word = FALSE): array
	{
		if (is_string($list))
		{
			$list = preg_split('`[\r\n]+`', $list, -1, PREG_SPLIT_NO_EMPTY);
		}

		if (is_array($list))
		{
			$subject = preg_replace('`[\x{0000}-\x{0020}\x{007F}-\x{00A0}]+`u', ' ', $subject);
			$subject = self::trimAll($subject);

			$str = self::removeAccents($subject);
			$replace = $word ? ['.', '\w*'] : ['.', '.*'];
			$regex = $word ? '(?:^|\W)(%s)(?:$|\W)' : '^(%s)$';

			foreach ($list as &$entry)
			{
				$ent = preg_quote($entry);
				$ent = str_replace(['\\?', '\\*'], $replace, $ent);
				$ent = self::removeAccents($ent);

				if (preg_match('`' . sprintf($regex, $ent) . '`ui', $str, $m))
				{
					$match = mb_substr($subject, mb_stripos($str, $m[1]), mb_strlen($m[1]));
					$test = preg_match('`^' . $ent . '$`ui', self::removeAccents($match));

					return
					[
						'entry' => $entry,
						'match' => $test ? $match : $m[1]
					];
				}
			}
		}

		return [];
	}

	/**
	 * Retire les accents d'une chaîne.
	 *
	 * @param string $str
	 * @param bool $transliterate
	 *
	 * @return string
	 */
	public static function removeAccents(string $str, bool $transliterate = FALSE): string
	{
		if ($transliterate && extension_loaded('intl'))
		{
			$transliterator = Transliterator::createFromRules(
				':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: NFC;'
			);
			if ($transliterator && is_string($str_trans = $transliterator->transliterate($str)))
			{
				return $str_trans;
			}
		}

		// Accents et ligatures les plus courants.
		$list =
		[
			'A' => ['À','Á','Â','Ã','Ä','Å','Ā','Ă','Ą','Ǎ','Ȁ'],
			'C' => ['Ç','Ć','Ĉ','Ċ','Č'],
			'D' => ['Ð','Ď','Đ'],
			'E' => ['È','É','Ê','Ë','Ē','Ė','Ę','Ě','Ȅ'],
			'G' => ['Ĝ','Ğ','Ġ'],
			'H' => ['Ĥ','Ħ'],
			'I' => ['Ì','Í','Î','Ï','Ī','İ','Į','Ǐ','Ȉ'],
			'J' => ['Ĵ'],
			'L' => ['Ĺ','Ľ','Ł'],
			'N' => ['Ñ','Ń','Ň','Ŋ'],
			'O' => ['Ò','Ó','Ô','Õ','Ö','Ø','Ō','Ő','Ǒ','Ȍ'],
			'R' => ['Ŕ','Ř','Ȑ','Ȓ'],
			'S' => ['Ś','Ŝ','Ş','Š','Ș'],
			'T' => ['Ť','Ŧ','Ț'],
			'U' => ['Ù','Ú','Û','Ü','Ū','Ů','Ű','Ų','Ǔ','Ȕ'],
			'W' => ['Ŵ','Ẁ','Ẃ','Ẅ'],
			'Y' => ['Ý','Ÿ','Ŷ','Ȳ','Ỳ'],
			'Z' => ['Ź','Ż','Ž'],

			'a' => ['à','á','â','ã','ä','å','ā','ă','ą','ǎ','ȁ'],
			'c' => ['ç','ć','ĉ','ċ','č'],
			'd' => ['ð','ď','đ'],
			'e' => ['è','é','ê','ë','ē','ė','ę','ě','ȅ'],
			'g' => ['ĝ','ğ','ġ'],
			'h' => ['ĥ','ħ'],
			'i' => ['ì','í','î','ï','ī','į','ı','ǐ','ȉ'],
			'j' => ['ĵ'],
			'l' => ['ĺ','ľ','ł'],
			'n' => ['ñ','ń','ň','ŋ'],
			'o' => ['ò','ó','ô','õ','ö','ø','ō','ő','ǒ','ȍ'],
			'r' => ['ŕ','ř','ȑ','ȓ'],
			's' => ['ś','ŝ','ş','š','ș'],
			't' => ['ť','ŧ','ț'],
			'u' => ['ù','ú','û','ü','ū','ů','ű','ų','ǔ','ȕ'],
			'w' => ['ŵ','ẁ','ẃ','ẅ'],
			'y' => ['ý','ÿ','ŷ','ȳ','ỳ'],
			'z' => ['ź','ż','ž'],

			'AE' => ['Æ','Ǣ','Ǽ','ᴁ'],
			'IJ' => ['Ĳ'],
			'LJ' => ['Ǉ'], 'LL' => ['Ỻ'], 'Lj' => ['ǈ'],
			'OE' => ['Œ','ɶ'],
			'SS' => ['ẞ'],
			'TH' => ['Þ'],
			'ae' => ['æ','ǣ','ǽ'],
			'ff' => ['ﬀ'], 'fi' => ['ﬁ'], 'fl' => ['ﬂ'],
			'hv' => ['ƕ'],
			'ij' => ['ĳ'],
			'lj' => ['ǉ'], 'll' => ['ỻ'],
			'oe' => ['œ'],
			'ss' => ['ß'], 'st' => ['ﬅ','ﬆ'],
			'th' => ['þ'],
			'ue' => ['ᵫ']
		];
		foreach ($list as $l => &$chars)
		{
			$str = str_replace($chars, $l, $str);
		}

		return $str;
	}

	/**
	 * Limite la longueur d'une chaîne de caractères.
	 *
	 * @param string $str
	 * @param int $limit
	 * @param string $marker
	 *
	 * @return string
	 */
	public static function strLimit(string $str, int $limit, string $marker = '...'): string
	{
		return mb_strimwidth($str, 0, $limit, $marker);
	}

	/**
	 * Supprime tous les types d'espaces et tous les caractères de contrôle
	 * au début et à la fin d'une chaîne.
	 *
	 * @param string $str
	 *
	 * @return string
	 */
	public static function trimAll(string $str): string
	{
		$str = preg_replace('`[\x{0000}-\x{0020}\x{007F}-\x{00A0}]+$`u', '', $str);
		$str = preg_replace('`^[\x{0000}-\x{0020}\x{007F}-\x{00A0}]+`u', '', (string) $str);

		return $str;
	}

	/**
	 * Retourne la regexp pour la partie d'URL $p.
	 *
	 * @param string $p
	 * @param bool $with_protocol
	 *
	 * @return string
	 */
	public static function URLRegexp(string $p = 'url', bool $with_protocol = TRUE): string
	{
		// Protocole.
		$protocol  = $with_protocol ? '(?:https?|ftp)://' : '';

		// IP (IPv4 et IPv6).
		$ip        = '(?:(?:[\da-f]{0,4}[.:]?){1,10}[\da-f]{0,4})';

		// Nom d'utilisateur et mot de passe.
		$user_pass = '(?:[-\w]+:[-\w]+@)?';

		// Serveur local.
		$local     = '(?:[a-z\d][-a-z\d]{0,62}(?<!-))';

		// Nom de domaine.
		$domain    = '(?:[a-z\d][-a-z\d]{0,62}(?<!-)\.?)+';

		// TLD.
		$tld       = '[a-z]{2,24}';

		// Numéro de port.
		$port      = '(?::(?:6553[0-5]|655[0-2]\d|65[0-4]\d\d|'
				   . '6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{1,3}|\d))?';

		// Chemin.
		$path      = '(?:/[-@&=+?%~./,;*a-z\d_#]*)?';

		// URL complète.
		$url       = $protocol . $user_pass
				   . '(?:' . $domain . $tld . '(?<![^/@]{256})|' . $ip . '|' . $local . ')'
				   . $port . $path
				   . '(?<![]).,;:!?])';

		return ${$p};
	}

	/**
	 * Convertit une chaîne en UTF-8.
	 *
	 * @param string $str
	 *   Chaîne à convertir.
	 *
	 * @return string
	 */
	public static function UTF8(string &$str): string
	{
		// On retourne la chaîne telle quelle si elle est déjà en UTF-8.
		if (self::isUTF8($str))
		{
			return $str;
		}

		// On tente de convertir en UTF-8.
		if (function_exists('iconv')
		&& ($str_iconv = iconv('ISO-8859-15', 'UTF-8', $str)) !== FALSE)
		{
			return $str_iconv;
		}

		// Si ça ne marche pas on retourne la chaîne originale.
		return $str;
	}

	/**
	 * Convertit un tableau en UTF-8.
	 *
	 * @param array $arr
	 *
	 * @return array
	 */
	public static function UTF8Array(array &$arr): array
	{
		foreach ($arr as &$i)
		{
			if (is_array($i))
			{
				$i = self::UTF8Array($i);
			}
			if (is_string($i))
			{
				$i = self::UTF8($i);
			}
		}

		return $arr;
	}
}
?>