<?php
declare(strict_types = 1);

/**
 * Gestion des catégories parentes.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Parents
{
	/**
	 * Séparateur des catégories parentes pour
	 * la colonne "cat_parents" de la table "categories".
	 *
	 * @var string
	 */
	const SEPARATOR = '.';



	/**
	 * Construit le chemin complet pour des catégories et fichiers.
	 * Ce n'est pas le chemin sur le disque, mais le chemin pour affichage
	 * dans la galerie, donc avec le nom des catégories parentes et non pas
	 * le nom des répertoires correspondants.
	 * C'est l'équivalent d'un "fil d'Ariane".
	 *
	 * @param array $ids
	 *    Identifiants des catégories et fichiers.
	 *    Le tableau doit avoir la forme suivante :
	 *    $ids = ['categories' => [5, 12, 78], 'items' => [41, 631]];
	 *
	 * @return array
	 */
	public static function getGalleryPath(array $ids): array
	{
		// Récupération des identifiants des catégories parentes.
		$cat_parents = [];
		$set_parents = function(&$data) use (&$cat_parents)
		{
			foreach ($data as &$i)
			{
				$cat_parents = array_merge(
					$cat_parents,
					explode(self::SEPARATOR, $i['cat_parents'] . $i['cat_id'])
				);
			}
			$cat_parents = array_values(array_unique($cat_parents));
		};
		if (!empty($ids['items']))
		{
			$sql = 'SELECT item_id,
						   item_name,
						   cat_id,
						   cat_parents
					  FROM {items}
				 LEFT JOIN {categories} USING (cat_id)
					 WHERE item_id IN (' . DB::inInt($ids['items']) . ')';
			if (!DB::execute($sql))
			{
				return [];
			}
			$item_infos = DB::fetchAll('item_id');
			$set_parents($item_infos);
		}
		if (!empty($ids['categories']))
		{
			$sql = 'SELECT cat_id,
						   cat_parents
					  FROM {categories}
					 WHERE cat_id IN (' . DB::inInt($ids['categories']) . ')';
			if (!DB::execute($sql))
			{
				return [];
			}
			$cat_infos = DB::fetchAll('cat_id');
			$set_parents($cat_infos);
		}
		if (!$cat_parents)
		{
			return [];
		}

		// Récupération du nom des catégories parentes.
		$sql = 'SELECT cat_id,
					   cat_name
				  FROM {categories}
				 WHERE cat_id IN (' . DB::inInt($cat_parents) . ')';
		if (!DB::execute($sql))
		{
			return [];
		}
		$cat_names = DB::fetchAll('cat_id', 'cat_name');

		// Construction du chemin.
		$gallery_path = [];
		if (isset($ids['categories']))
		{
			foreach ($ids['categories'] as &$id)
			{
				$categories = explode(
					self::SEPARATOR,
					substr($cat_infos[$id]['cat_parents'], 2) . $id
				);
				foreach ($categories as &$p)
				{
					$p = $cat_names[$p];
				}
				$gallery_path['categories'][$id] = implode(' / ', $categories);
			}
		}
		if (isset($ids['items']))
		{
			foreach ($ids['items'] as &$id)
			{
				$categories = explode(
					self::SEPARATOR,
					substr($item_infos[$id]['cat_parents'], 2) . $item_infos[$id]['cat_id']
				);
				foreach ($categories as &$p)
				{
					$p = $cat_names[$p];
				}
				$gallery_path['items'][$id] = implode(' / ', $categories)
					. ' / ' . $item_infos[$id]['item_name'];
			}
		}

		return $gallery_path;
	}

	/**
	 * Récupère les informations utiles des
	 * catégories $categories_id.
	 *
	 * @param array $categories_id
	 *
	 * @return array
	 */
	public static function getInfos(array $categories_id): array
	{
		static $cache = [];

		$data = [];
		foreach ($categories_id as &$cat_id)
		{
			if (array_key_exists($cat_id, $cache))
			{
				$data[$cat_id] = $cache[$cat_id];
			}
			else
			{
				$sql = 'SELECT cat_id,
							   thumb_id,
							   cat_name,
							   cat_url,
							   cat_votable,
							   cat_commentable,
							   cat_downloadable,
							   cat_uploadable,
							   cat_creatable,
							   cat_a_subalbs + cat_a_subcats +
							   cat_d_subalbs + cat_d_subcats AS cat_subs,
							   cat_a_subalbs + cat_a_subcats AS cat_a_subs,
							   cat_orderby,
							   cat_filemtime,
							   CASE WHEN cat_filemtime IS NULL
									THEN "category" ELSE "album"
									 END AS cat_type
						  FROM {categories}
						 WHERE cat_id IN (' . DB::inInt($categories_id) . ')
					  ORDER BY LENGTH(cat_path) ASC';
				if (!DB::execute($sql))
				{
					return [];
				}
				$data = DB::fetchAll('cat_id');

				foreach ($data as $cat_id => &$i)
				{
					if (!array_key_exists($cat_id, $cache))
					{
						$cache[$cat_id] = $i;
					}
				}

				return $data;
			}
		}

		return $data;
	}

	/**
	 * Définit les réglages des objets $objects_infos
	 * en fonction de leurs parents.
	 *
	 * @param array $objects_infos
	 *
	 * @return void
	 */
	public static function settings(array &$objects_infos): void
	{
		foreach ($objects_infos as &$i)
		{
			$parents = Category::getParentsIdArray($i['cat_parents']);
			array_shift($parents);
			$i['parents_id'] = $parents;
			foreach ($parents as &$id)
			{
				$parents_id[$id] = 1;
			}
		}
		if (!empty($parents_id))
		{
			$parents_infos = self::getInfos(array_keys($parents_id));
		}
		$perms = ['commentable', 'creatable', 'downloadable', 'uploadable', 'votable'];
		foreach ($objects_infos as &$i)
		{
			foreach ($perms as &$p)
			{
				$i['parent_' . $p] = '1';
			}

			foreach ($i['parents_id'] as &$id)
			{
				// Permissions.
				if ($id)
				{
					foreach ($perms as &$p)
					{
						if ($parents_infos[$id]['cat_' . $p] != '1')
						{
							$i['parent_' . $p] = '0';
						}
					}
				}
			}

			unset($i['parents_id']);
		}
	}

	/**
	 * Met à jour les informations suivantes des catégories $parents_id :
	 *
	 * - Choisi une nouvelle vignette lorsque le fichier correspondant
	 *   à la vignette de la catégorie a été supprimé ou désactivé.
	 *
	 * - Indique une catégorie comme vide lorsqu'aucun fichier de
	 *   cette catégorie n'est activé.
	 *
	 * - Active une catégorie lorsqu'au moins un fichier activé se
	 *   trouve dans cette catégorie.
	 *
	 * - Met à jour la date de publication du dernier fichier de la catégorie.
	 *
	 * - Incrémente/décrémente le nombre d'albums activés/désactivés pour
	 *   les catégories parentes si un album est désactivé mais qu'il
	 *   contient des fichiers activés.
	 *
	 * @param array $parents_id
	 * @param int $status
	 *
	 * @return bool
	 *   TRUE si succès, FALSE si échec.
	 */
	public static function updateInfos(array $parents_id, int $status = 0): bool
	{
		// On récupère les informations utiles des catégories.
		$sql = 'SELECT cat_id,
					   cat_path,
					   thumb_id,
					   cat_tb_params,
					   cat_parents,
					   cat_a_albums,
					   cat_d_albums,
					   cat_a_images + cat_a_videos AS cat_a_items,
					   cat_d_images + cat_d_videos AS cat_d_items,
					   cat_crtdt,
					   cat_status,
					   cat_filemtime,
					   (SELECT item_status
					      FROM {items}
						 WHERE thumb_id = item_id
						   AND item_path LIKE cat_path || "/%") AS item_status
				  FROM {categories}
				 WHERE cat_id IN (' . DB::inInt($parents_id) . ')
			  ORDER BY LENGTH(cat_parents) DESC';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$categories = DB::fetchAll('cat_id');

		// La catégorie 1 doit toujours être en dernier.
		$cat_one = $categories[1];
		unset($categories[1]);
		$categories[1] = $cat_one;

		// On traite chaque catégorie.
		$albums_add = 0;
		$cat_albums = [];
		foreach ($categories as &$i)
		{
			$cat_path = $i['cat_id'] > 1 ? DB::likeEscape($i['cat_path']) . '/%' : '%';
			$cols =
			[
				'thumb_id' => $i['thumb_id'],
				'cat_tb_params' => $i['cat_tb_params'],
				'cat_a_albums' => $i['cat_a_albums'],
				'cat_d_albums' => $i['cat_d_albums'],
				'cat_status' => $i['cat_id'] > 1
					? (($i['cat_a_items'] > 0 || $i['cat_a_albums'] > 0 || $status) ? '1' : '0')
					: '1',
				'cat_id' => $i['cat_id']
			];

			if ($i['cat_id'] == 1)
			{
				goto lastpubdt;
			}

			// Si la catégorie ne contient aucun fichier,
			// on supprime la référence à une vignette.
			if (($i['cat_a_items'] + $i['cat_d_items']) == 0)
			{
				$cols['thumb_id'] = -1;
				$cols['cat_tb_params'] = NULL;
				goto lastpubdt;
			}

			// S'il s'agit d'une vignette d'une image externe,
			// alors on laisse cette vignette.
			if ($i['thumb_id'] == 0)
			{
				goto lastpubdt;
			}

			// On choisi une nouvelle vignette pour la catégorie si :
			// - Le fichier de la vignette de la catégorie n'existe pas dans
			//   cette catégorie.
			// - Ou le fichier correspondant à cette vignette est désactivé
			//   alors que la catégorie contient des fichiers activés.
			if (!in_array($i['item_status'], ['-1', '0', '1'])
			|| ($i['cat_a_items'] > 0 && Item::getStatus($i['item_status']) == '0'))
			{
				$sql = 'SELECT item_id
						  FROM {items}
						 WHERE item_path LIKE ?
					  ORDER BY item_status DESC,
							   item_id DESC
						 LIMIT 1';
				if (!DB::execute($sql, $cat_path))
				{
					return FALSE;
				}
				$cols['thumb_id'] = DB::fetchVal();

				// On supprime les coordonnées de rognage
				// de la vignette précédente.
				$cols['cat_tb_params'] = NULL;
			}

			// Date de publication du fichier le plus récent.
			lastpubdt:
			$sql = 'SELECT item_pubdt
					  FROM {items}
					 WHERE item_path LIKE ?
					   AND item_status = "1"
				  ORDER BY item_pubdt DESC
					 LIMIT 1';
			if (!DB::execute($sql, $cat_path))
			{
				return FALSE;
			}
			$cat_lastpubdt = DB::fetchVal();
			$cols['cat_lastpubdt']
				= preg_match('`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`', (string) $cat_lastpubdt)
				? $cat_lastpubdt
				: NULL;

			$params[$i['cat_id']] = $cols;

			// Modification du nombre d'albums activés et désactivés.
			if (($active = ($i['cat_filemtime'] && !$i['cat_status'] && $i['cat_a_items']))
			 || ($i['cat_filemtime'] && $i['cat_status'] && !$i['cat_a_items']))
			{
				foreach (Category::getParentsIdArray($i['cat_parents']) as $cat_id)
				{
					if (!isset($cat_albums[$cat_id]))
					{
						$cat_albums[$cat_id]['cat_a_albums'] = 0;
						$cat_albums[$cat_id]['cat_d_albums'] = 0;
					}
					$cat_albums[$cat_id]['cat_a_albums'] += $active ? 1 : -1;
					$cat_albums[$cat_id]['cat_d_albums'] += $active ? -1 : 1;
				}
			}
		}

		if (empty($params))
		{
			return TRUE;
		}

		// Modification du nombre d'albums activés/désactivés des catégories.
		foreach ($params as $cat_id => &$p)
		{
			if (isset($cat_albums[$cat_id]))
			{
				$p['cat_a_albums'] += $cat_albums[$cat_id]['cat_a_albums'];
				$p['cat_d_albums'] += $cat_albums[$cat_id]['cat_d_albums'];
			}
		}

		// Mise à jour de la base de données.
		$sql = 'UPDATE {categories}
				   SET thumb_id = :thumb_id,
				       cat_tb_params = :cat_tb_params,
					   cat_a_albums = :cat_a_albums,
					   cat_d_albums = :cat_d_albums,
					   cat_lastpubdt = :cat_lastpubdt,
				       cat_status = :cat_status
				 WHERE cat_id = :cat_id';
		return DB::execute($sql, array_values($params));
	}

	/**
	 * Mise à jour des statistiques des catégories parentes.
	 *
	 * @param array $update
	 *   Statistiques à additionner ou soustraire aux catégories.
	 * @param string $a_sign
	 *   Additionne ('+') ou soustrait ('-') les informations des
	 *   fichiers activés. Chaîne vide si aucune modification souhaitée.
	 * @param string $d_sign
	 *   Additionne ('+') ou soustrait ('-') les informations des
	 *   fichiers désactivés. Chaîne vide si aucune modification souhaitée.
	 * @param array $parents_id
	 *   Identifiants des catégories parentes à mettre à jour.
	 *
	 * @return bool
	 *   TRUE si succès, FALSE si échec.
	 */
	public static function updateStats(array $update,
	string $a_sign, string $d_sign, array $parents_id): bool
	{
		foreach(['a' => $a_sign, 'd' => $d_sign] as $l => $s)
		{
			if (!in_array($s, ['+', '-']))
			{
				continue;
			}

			$albums = (int) $update[$l]['albums'];
			$comments = (int) $update[$l]['comments'];
			$favorites = (int) $update[$l]['favorites'];
			$hits = (int) $update[$l]['hits'];
			$images = (int) $update[$l]['images'];
			$rating = (float) $update[$l]['rating'];
			$size = (int) $update[$l]['size'];
			$videos = (int) $update[$l]['videos'];
			$votes = (int) $update[$l]['votes'];

			$sql_set[] = "cat_{$l}_albums = CASE
				WHEN cat_filemtime IS NULL
				THEN cat_{$l}_albums $s $albums
				ELSE 0
				 END";
			$sql_set[] = "cat_{$l}_comments = cat_{$l}_comments $s $comments";
			$sql_set[] = "cat_{$l}_favorites = cat_{$l}_favorites $s $favorites";
			$sql_set[] = "cat_{$l}_hits = cat_{$l}_hits $s $hits";
			$sql_set[] = "cat_{$l}_images = cat_{$l}_images $s $images";
			if ($rating)
			{
				$sql_set[] = "cat_{$l}_rating = CASE
					WHEN cat_{$l}_votes $s $votes > 0
					THEN ((cat_{$l}_rating * cat_{$l}_votes) $s ($rating * $votes))
					   / (cat_{$l}_votes $s $votes)
					ELSE 0
					 END";
			}
			$sql_set[] = "cat_{$l}_size = cat_{$l}_size $s $size";
			$sql_set[] = "cat_{$l}_videos = cat_{$l}_videos $s $videos";
			$sql_set[] = "cat_{$l}_votes = cat_{$l}_votes $s $votes";
		}

		$sql = 'UPDATE {categories}
				   SET ' . implode(', ', $sql_set) . '
		         WHERE cat_id IN (' . DB::inInt($parents_id) . ')';
		return DB::execute($sql);
	}

	/**
	 * Met à jour le nombre de sous-albums et de sous-catégories
	 * pour les catégories $categories_id.
	 *
	 * @param array $categories_id
	 *
	 * @return bool
	 *   TRUE si succès, FALSE si échec.
	 */
	public static function updateSubCats(array $categories_id): bool
	{
		$sql = 'SELECT parent_id, cat_status, cat_filemtime
				  FROM {categories}
				 WHERE parent_id IN (' . DB::inInt($categories_id) . ')
				   AND cat_id > 1';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$cat_infos = DB::fetchAll();

		foreach ($categories_id as &$id)
		{
			$params[$id] = [0, 0, 0, 0, $id];
		}
		foreach ($cat_infos as &$d)
		{
			$i = 0;
			foreach ([1, 0] as $s)
			{
				if ($d['cat_status'] == $s)
				{
					$params[$d['parent_id']][$i + (int) (!$d['cat_filemtime'])]++;
				}
				$i = 2;
			}
		}

		$sql = 'UPDATE {categories}
				   SET cat_a_subalbs = ?, cat_a_subcats = ?,
					   cat_d_subalbs = ?, cat_d_subcats = ?
				 WHERE cat_id = ?';
		return DB::execute($sql, array_values($params));
	}
}
?>