<?php
declare(strict_types = 1);

/**
 * Gestion des tags.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Tags
{
	/**
	 * Crée un ou plusieurs tags.
	 *
	 * @param mixed $tags
	 *
	 * @return int
	 *   Retourne le nombre de tags ajoutés,
	 *   ou -1 en cas d'erreur.
	 */
	public static function add($tags): int
	{
		if (!is_array($tags))
		{
			$tags = [$tags];
		}

		// Préparation des paramètres.
		$new_tags = [];
		$params = [];
		foreach ($tags as &$tag_name)
		{
			$tag_name = Utility::trimAll($tag_name);
			$tag_name = str_replace(',', '', $tag_name);
			if (!Utility::isEmpty($tag_name))
			{
				$name = mb_strtolower($tag_name);
				$new_tags[$name] = [$tag_name, App::getURLName($tag_name)];
				$params[] = [$name];
			}
		}
		if (!$new_tags)
		{
			return 0;
		}

		// On ne conserve que les tags qui ne sont pas déjà présents
		// en base de données avec une casse différente.
		$sql = 'SELECT LOWER(tag_name) FROM {tags} WHERE LOWER(tag_name) = LOWER(?)';
		$tags_db = ['fetchVal'];
		if (!DB::execute($sql, $params, [], $tags_db))
		{
			return -1;
		}
		foreach ($new_tags as $k => &$i)
		{
			if (in_array($k, $tags_db))
			{
				unset($new_tags[$k]);
			}
		}
		if (!$new_tags)
		{
			return 0;
		}

		// Mise à jour de la base de données.
		$sql = 'INSERT IGNORE INTO {tags} (tag_name, tag_url) VALUES (?, ?)';
		if (!DB::execute($sql, array_values($new_tags)))
		{
			return -1;
		}

		return DB::rowCount();
	}

	/**
	 * Supprime un ou plusieurs tags.
	 *
	 * @param mixed $tags_id
	 *   Identifiant des tags.
	 *
	 * @return int
	 *   Retourne le nombre de tags supprimés,
	 *   ou -1 en cas d'erreur.
	 */
	public static function delete($tags_id): int
	{
		if (!is_array($tags_id))
		{
			$tags_id = [$tags_id];
		}
		if (!$tags_id)
		{
			return 0;
		}

		if (!DB::execute('DELETE FROM {tags} WHERE tag_id IN (' . DB::inInt($tags_id) . ')'))
		{
			return -1;
		}

		return DB::rowCount();
	}

	/**
	 * Édite les informations de plusieurs tags.
	 *
	 * @param array $update
	 *   Informations de mise à jour.
	 *
	 * @return int
	 *   Retourne le nombre de tags affectés,
	 *   ou -1 en cas d'erreur.
	 */
	public static function edit(array $update): int
	{
		if (!$update)
		{
			return 0;
		}

		// Début de la transaction.
		if (!DB::beginTransaction())
		{
			return DB::rollback(-1);
		}

		// Récupération des informations utiles des tags.
		$tags_id = DB::inInt(array_keys($update));
		if (!DB::execute("SELECT * FROM {tags} WHERE tag_id IN ($tags_id)"))
		{
			return DB::rollback(-1);
		}
		$tags = DB::fetchAll();

		// Pour chaque tag.
		$tags_edited = 0;
		foreach ($tags as &$i)
		{
			$params = [];
			$change_tag_name = FALSE;

			// On détermine les informations à mettre à jour.
			foreach (['name', 'url'] as $col)
			{
				if (isset($update[$i['tag_id']][$col]))
				{
					$str = (string) $update[$i['tag_id']][$col];
					$str = Utility::trimAll($str);
					$str = str_replace(',', '', $str);
					if ($col == 'url')
					{
						$str = App::getURLName($str);
					}
					if (!Utility::isEmpty($str) && $str !== $i['tag_' . $col])
					{
						$params['tag_' . $col] = $str;
						if ($col == 'name')
						{
							$change_tag_name = TRUE;
						}
					}
				}
			}

			// On effectue la mise à jour du tag.
			if (!$params)
			{
				continue;
			}
			foreach (['id', 'name', 'url'] as $col)
			{
				if (!isset($params['tag_' . $col]))
				{
					$params['tag_' . $col] = $i['tag_' . $col];
				}
			}
			$sql = 'SELECT COUNT(*)
					  FROM (SELECT * FROM {tags}) AS t
					 WHERE tag_id != :tag_id
					   AND LOWER(tag_name) = LOWER(:tag_name)';
			$sql = "UPDATE {tags}
					   SET tag_name = :tag_name,
						   tag_url = :tag_url
					 WHERE tag_id = :tag_id
					   AND ($sql) = 0";
			if (!DB::execute($sql, $params))
			{
				return DB::rollback(-1);
			}
			$row_count = DB::rowCount();
			$tags_edited = $tags_edited + $row_count;

			// Si aucune ligne affectée et nom du tag modifié,
			// c'est qu'il existe déjà un tag du même nom.
			// Donc, on fusionne les deux tags.
			if (!$row_count && $change_tag_name)
			{
				// On récupère l'identifiant du tag existant.
				$sql = 'SELECT tag_id FROM {tags} WHERE LOWER(tag_name) = LOWER(?)';
				if (!DB::execute($sql, $params['tag_name']))
				{
					return DB::rollback(-1);
				}
				$new_tag_id = DB::fetchVal();

				// On associe ce tag aux fichiers du tag courant.
				$sql = 'SELECT COUNT(*)
						  FROM (SELECT * FROM {tags_items}) AS t2
						 WHERE t1.item_id = t2.item_id
						   AND tag_id = :new_tag_id';
				$sql = "UPDATE {tags_items} AS t1
						   SET tag_id = :new_tag_id
						 WHERE tag_id = :old_tag_id
						   AND ($sql) = 0";
				$params =
				[
					'new_tag_id' => $new_tag_id,
					'old_tag_id' => $i['tag_id']
				];
				if (!DB::execute($sql, $params))
				{
					return DB::rollback(-1);
				}

				// On supprime le tag courant.
				if (!DB::execute('DELETE FROM {tags} WHERE tag_id = ?', $i['tag_id']))
				{
					return DB::rollback(-1);
				}

				$tags_edited++;
			}
		}

		// Exécution de la transaction.
		if (!DB::commitTransaction())
		{
			return DB::rollback(-1);
		}

		return $tags_edited;
	}

	/**
	 * Ajoute des tags sur des fichiers.
	 *
	 * @param array $tags_add
	 *   Tableau ayant pour valeurs des tableaux de ce type :
	 *   ['item_id' => $item_id, 'tag_name' => $tag_name]
	 *
	 * @return int
	 *   Retourne le nombre de fichiers affectés,
	 *   ou -1 en cas d'erreur.
	 */
	public static function itemsAdd(array $tags_add): int
	{
		if (!$tags = self::_getValidTags($tags_add, 'add'))
		{
			return 0;
		}

		// Début de la transaction.
		if (!($in_transaction = DB::inTransaction()) && !DB::beginTransaction())
		{
			return -1;
		}

		// On enregistre les tags en base de données.
		$items_updated = [];
		$params = [];
		foreach ($tags as &$i)
		{
			$i['tag_url'] = App::getURLName($i['tag_name']);
			$params[] = [$i['tag_name'], $i['tag_url']];
		}
		$sql = 'INSERT IGNORE INTO {tags} (tag_name, tag_url) VALUES (?, ?)';
		if (!DB::execute($sql, $params))
		{
			return -1;
		}

		// On récupère l'identifiant des tags.
		$params = array_column($tags, 'tag_name');
		$sql_in = str_repeat(', ?', count($params) - 1);
		$sql = "SELECT tag_id, tag_name FROM {tags} WHERE tag_name IN (?$sql_in)";
		if (!DB::execute($sql, $params))
		{
			return -1;
		}
		$tags_url_id = DB::fetchAll('tag_name', 'tag_id');

		// On enregistre les associations tag - fichier.
		$params = [];
		foreach ($tags as &$i)
		{
			if (array_key_exists($i['tag_name'], $tags_url_id))
			{
				$params[] = [$tags_url_id[$i['tag_name']], $i['item_id']];
			}
		}
		$sql = 'INSERT IGNORE INTO {tags_items} (tag_id, item_id) VALUES (?, ?)';
		if (!DB::execute($sql, $params))
		{
			return -1;
		}

		// Exécution de la transaction.
		if (!$in_transaction && !DB::commitTransaction())
		{
			return -1;
		}

		return count(array_unique(array_column($tags, 'item_id')));
	}

	/**
	 * Supprime des tags sur des fichiers.
	 *
	 * @param array $delete_tags_items
	 *   Tableau contenant les associations
	 *   "identifiant de fichier" - "nom du tag".
	 *
	 * @return int
	 *   Retourne le nombre de fichiers affectés,
	 *   ou -1 en cas d'erreur.
	 */
	public static function itemsDelete(array $delete_tags_items): int
	{
		if (!$tags = self::_getValidTags($delete_tags_items, 'delete'))
		{
			return 0;
		}

		$sql = 'SELECT tag_id FROM {tags} WHERE LOWER(tag_name) = LOWER(:tag_name)';
		$sql = "DELETE FROM {tags_items} WHERE tag_id = ($sql) AND item_id = :item_id";
		if (!DB::execute($sql, array_values($tags)))
		{
			return -1;
		}

		return count(array_unique(array_column($tags, 'item_id')));
	}

	/**
	 * Supprime tous les tags sur un ou plusieurs fichiers.
	 *
	 * @param array $items_id
	 *
	 * @return int
	 *   Retourne le nombre de fichiers affectés,
	 *   ou -1 en cas d'erreur.
	 */
	public static function itemsDeleteAll(array $items_id): int
	{
		$items_id = DB::inInt($items_id);
		$sql = "SELECT DISTINCT item_id FROM {tags_items} WHERE item_id IN ($items_id)";
		if (!DB::execute($sql))
		{
			return -1;
		}
		if (!count($items_tags = DB::fetchCol('item_id')))
		{
			return 0;
		}

		$sql = 'DELETE FROM {tags_items} WHERE item_id IN (' . DB::inInt($items_tags) . ')';
		if (!DB::execute($sql))
		{
			return -1;
		}

		return count($items_tags);
	}

	/**
	 * Nettoie une chaîne de tags séparés par des virgules.
	 *
	 * @param string $tags
	 *
	 * @return string
	 */
	public static function sanitize(string $tags): string
	{
		$tags = preg_replace('`[\x{0000}-\x{0020}\x{007F}-\x{00A0}]+`u', ' ', $tags);
		$tags = preg_replace('`\s+`', ' ', $tags);
		$tags = preg_replace('`(^[\s,]+|[\s,]+$)`', '', $tags);
		$tags = preg_replace('`\s*,\s*`', ',', $tags);
		$tags = preg_replace('`,+`', ',', $tags);

		return $tags;
	}



	/**
	 * Filtre la liste des associations tag - fichier pour ne garder
	 * que celles dont les valeurs sont valides et celles qui :
	 *    - N'existent pas déjà (si on ajoute des tags).
	 *    - Existent déjà (si on supprime des tags).
	 *
	 * @param array $tags_check
	 * @param string $action
	 *
	 * @return array
	 */
	private static function _getValidTags(array $tags_check, string $action): array
	{
		// Vérifications des valeurs.
		$tags = [];
		foreach ($tags_check as &$i)
		{
			$i['tag_name'] = str_replace(',', '', $i['tag_name']);
			$i['tag_name'] = Utility::trimAll($i['tag_name']);
			if (!Utility::isEmpty($i['tag_name']) && mb_strlen($i['tag_name']) < 256
			&& (int) $i['item_id'] > 0)
			{
				$tags[$i['item_id'] . '|' . mb_strtolower($i['tag_name'])] = $i;
			}
		}
		$tags = array_values($tags);

		// On recherche des tags existants mais avec une casse différente.
		$params = [];
		foreach (array_column($tags, 'tag_name') as &$tag)
		{
			$params[] = [$tag];
		}
		$sql = 'SELECT tag_name FROM {tags} WHERE LOWER(tag_name) = LOWER(?)';
		$tags_name = ['fetchRow'];
		if (!DB::execute($sql, $params, [], $tags_name))
		{
			return [];
		}
		foreach ($tags_name as $k => &$i)
		{
			if (array_key_exists('tag_name', $i))
			{
				$tags[$k]['tag_name'] = $i['tag_name'];
			}
		}

		// Vérifications des associations tag - fichier.
		$sql = 'SELECT item_id
				  FROM {tags_items}
			 LEFT JOIN {tags} USING (tag_id)
				 WHERE item_id = :item_id
				   AND LOWER(tag_name) = LOWER(:tag_name)';
		$items_id = ['fetchVal'];
		if (!DB::execute($sql, $tags, [], $items_id))
		{
			return [];
		}
		foreach ($tags as $k => &$i)
		{
			if (($action == 'add' && $items_id[$k])
			 || ($action == 'delete' && !$items_id[$k]))
			{
				unset($tags[$k]);
			}
		}

		return $tags;
	}
}
?>