<?php
declare(strict_types = 1);

/**
 * Gestion des votes.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Rating
{
	/**
	 * Ajoute ou modifie une note sur un fichier,
	 * sans tenir compte des permissions d'accès
	 * ni de l'état du fichier.
	 *
	 * @param int $item_id
	 *   Identifiant du fichier.
	 * @param int $user_id
	 *   Identifiant de l'utilisateur.
	 * @param int $rating
	 *   Note, comprise entre 1 et 5.
	 *
	 * @return int
	 *   Retourne l'identifiant du vote si la note
	 *   a été ajoutée ou modifiée avec succès,
	 *   0 si aucune modification n'a été affectuée,
	 *   ou -1 en cas d'erreur.
	 */
	public static function add(int $item_id, int $user_id, int $rating): int
	{
		if ($rating < 1 || $rating > 5)
		{
			return 0;
		}

		$post = ['item_id' => $item_id, 'rating' => $rating];

		// Vérification par liste noire des adresses IP.
		if (App::blacklists('vote_add', '', '', '', $post) !== TRUE)
		{
			return 0;
		}

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

		// On détermine si l'utilisateur a déjà voté le fichier.
		$update = 0;
		if ($user_id == 2)
		{
			$vote_cookie = Auth::$prefs->read('rating');
			if (preg_match('`^[a-z\d]{40}$`i', $vote_cookie))
			{
				$sql = 'SELECT vote_id,
							   vote_rating
						  FROM {votes}
						 WHERE item_id = ?
						   AND vote_cookie = ?
						 LIMIT 1';
				if (!DB::execute($sql, [$item_id, $vote_cookie]))
				{
					return DB::rollback(-1);
				}
				$db_vote = DB::fetchRow();
				$update = count($db_vote);
			}
			else
			{
				$vote_cookie = 0;
			}
		}
		else
		{
			$sql = 'SELECT vote_id,
						   vote_rating
					  FROM {votes}
					 WHERE item_id = ?
					   AND user_id = ?
					 LIMIT 1';
			if (!DB::execute($sql, [$item_id, $user_id]))
			{
				return DB::rollback(-1);
			}
			$db_vote = DB::fetchRow();
			$update = count($db_vote);
		}

		// Récupération des identifiants des catégories parentes.
		$sql = 'SELECT item_status,
					   cat.cat_id,
					   cat_parents
				  FROM {items}
			 LEFT JOIN {categories} AS cat USING (cat_id)
				 WHERE item_id = ?';
		if (!DB::execute($sql, $item_id))
		{
			return DB::rollback(-1);
		}
		if (!$infos = DB::fetchRow())
		{
			return 0;
		}
		$sql_parents_id = DB::inInt(Category::getParentsIdArray(
			$infos['cat_parents'], $infos['cat_id']
		));
		$col = $infos['item_status'] == '1' ? 'a' : 'd';

		// On met à jour la note de l'utilisateur.
		if ($update)
		{
			// Seulement si elle est différente.
			if ($db_vote['vote_rating'] == $rating)
			{
				return (int) $db_vote['vote_id'];
			}

			// On met à jour la table des votes.
			$sql = 'UPDATE {votes} SET vote_rating = ?, vote_date = NOW() WHERE vote_id = ?';
			if (!DB::execute($sql, [$rating, $db_vote['vote_id']]))
			{
				return DB::rollback(-1);
			}
			$vote_id = $db_vote['vote_id'];

			// On met à jour la note moyenne du fichier.
			$sql = 'UPDATE {items}
					   SET item_rating = CASE WHEN item_votes = 1
							THEN :new_rating
							ELSE (((((item_rating * item_votes) - :old_rating)
								/ (item_votes - 1)) * (item_votes - 1)) + :new_rating)
								/ item_votes
							END
					 WHERE item_id = :item_id';
			$params =
			[
				'item_id' => $item_id,
				'new_rating' => $rating,
				'old_rating' => (int) $db_vote['vote_rating']
			];
			if (!DB::execute($sql, $params))
			{
				return DB::rollback(-1);
			}

			// On met à jour la note moyenne des catégories parentes.
			$sql = "UPDATE {categories}
					   SET cat_{$col}_rating = CASE WHEN cat_{$col}_votes = 1
							THEN :new_rating
							ELSE (((((cat_{$col}_rating * cat_{$col}_votes)
									- :old_rating)
								/ (cat_{$col}_votes - 1)) * (cat_{$col}_votes - 1))
									+ :new_rating)
								/ cat_{$col}_votes
							END
					 WHERE cat_id IN ($sql_parents_id)";
			$params =
			[
				'new_rating' => $rating,
				'old_rating' => (int) $db_vote['vote_rating']
			];
			if (!DB::execute($sql, $params))
			{
				return DB::rollback(-1);
			}

			// Log d'activité.
			App::logActivity('vote_change', '', $post);
		}

		// On ajoute la note de l'utilisateur.
		else
		{
			// Si l'utilisateur ne possède aucun code de cookie,
			// on en génère un nouveau.
			if ($user_id == 2 && empty($vote_cookie))
			{
				$vote_cookie = Security::key(40);
				Auth::$prefs->add('rating', $vote_cookie);
			}

			// On enregistre le nouveau vote dans la table des votes.
			$sql = 'INSERT INTO {votes} (user_id, item_id, vote_rating,
				vote_date, vote_ip, vote_cookie) VALUES (?, ?, ?, NOW(), ?, ?)';
			$params = [$user_id, $item_id, $rating, $_SERVER['REMOTE_ADDR'], $vote_cookie ?? ''];
			$seq = ['table' => '{votes}', 'column' => 'vote_id'];
			if (!DB::execute($sql, $params, $seq))
			{
				return DB::rollback(-1);
			}
			$vote_id = DB::lastInsertId();

			// On met à jour le nombre de votes et la moyenne des notes
			// dans la table des fichiers.
			$sql = 'UPDATE {items}
					   SET item_rating = ((item_rating * item_votes) + ?)
							/ (item_votes + 1),
						   item_votes = item_votes + 1
					 WHERE item_id = ?';
			if (!DB::execute($sql, [$rating, $item_id]))
			{
				return DB::rollback(-1);
			}

			// On met à jour le nombre de votes et la moyenne des notes
			// dans la table des catégories.
			$sql = "UPDATE {categories}
					   SET cat_{$col}_rating =
							((cat_{$col}_rating * cat_{$col}_votes) + ?)
							/ (cat_{$col}_votes + 1),
						   cat_{$col}_votes = cat_{$col}_votes + 1
					 WHERE cat_id IN ($sql_parents_id)";
			if (!DB::execute($sql, $rating))
			{
				return DB::rollback(-1);
			}

			// Log d'activité.
			App::logActivity('vote_add', '', ['item_id' => $item_id, 'rating' => $rating]);
		}

		// Vérification des stats.
		if (CONF_DEV_MODE && Maintenance::dbStats() !== 0)
		{
			trigger_error('Gallery stats error.', E_USER_WARNING);
			return DB::rollback(-1);
		}

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

		return (int) $vote_id;
	}

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

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

		// Récupération des informations utiles des fichiers.
		$sql = 'SELECT v.vote_id,
					   v.vote_rating,
					   i.item_id,
					   i.item_status,
					   cat.cat_id,
					   cat.cat_parents
				  FROM {votes} AS v,
					   {items} AS i,
					   {categories} AS cat
				 WHERE v.item_id = i.item_id
				   AND i.cat_id = cat.cat_id
				   AND v.vote_id IN (' . DB::inInt($votes_id) . ')';
		if (!DB::execute($sql))
		{
			return DB::rollback(-1);
		}
		if (!$infos = DB::fetchAll())
		{
			return 0;
		}
		$items_votes = [];
		$items_infos = [];
		foreach ($infos as &$i)
		{
			$items_votes[(int) $i['item_id']][(int) $i['vote_id']] = (int) $i['vote_rating'];
			$items_infos[(int) $i['item_id']] =
			[
				'item_status' => $i['item_status'],
				'cat_id' => (int) $i['cat_id'],
				'cat_parents' => $i['cat_parents']
			];
		}

		// Suppression des votes.
		$sql = 'DELETE FROM {votes} WHERE vote_id IN (' . DB::inInt($votes_id) . ')';
		if (!DB::execute($sql))
		{
			return DB::rollback(-1);
		}
		if (!$deleted = DB::rowCount())
		{
			return 0;
		}

		// Mise à jour des statistiques.
		$sql = [];
		$parents_id = [];
		foreach ($items_votes as $item_id => &$votes_id)
		{
			$i =& $items_infos[$item_id];

			// Paramètres.
			$col = $i['item_status'] == '1' ? 'a' : 'd';
			$count = count($votes_id);
			$sum = array_sum($votes_id);

			// Mise à jour du fichier.
			$sql = "UPDATE {items}
					   SET item_rating = CASE
							WHEN item_votes - $count > 0
							THEN ((item_rating * item_votes) - $sum)
								/ (item_votes - $count)
							ELSE 0
							 END,
						   item_votes = item_votes - $count
					 WHERE item_id = $item_id";
			if (!DB::execute($sql))
			{
				return DB::rollback(-1);
			}

			// Mise à jour des catégories parentes.
			$sql_parents_id = DB::inInt(Category::getParentsIdArray(
				$i['cat_parents'], $i['cat_id']
			));
			$sql = "UPDATE {categories}
					   SET cat_{$col}_rating = CASE
							WHEN cat_{$col}_votes - $count > 0
							THEN ((cat_{$col}_rating * cat_{$col}_votes) - $sum)
								/ (cat_{$col}_votes - $count)
							ELSE 0
							 END,
						   cat_{$col}_votes = cat_{$col}_votes - $count
					 WHERE cat_id IN ($sql_parents_id)";
			if (!DB::execute($sql))
			{
				return DB::rollback(-1);
			}
		}

		// Vérification des stats.
		if (CONF_DEV_MODE && Maintenance::dbStats() !== 0)
		{
			trigger_error('Gallery stats error.', E_USER_WARNING);
			return DB::rollback(-1);
		}

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

		return $deleted;
	}

	/**
	 * Retourne une note arrondie et formatée sous forme d'un tableau.
	 *
	 * @param mixed $rating
	 * @param int $length
	 *
	 * @return array
	 */
	public static function formatArray($rating, int $length = 5): array
	{
		$rating = (float) $rating;
		$round = round($rating * 2) / 2;
		$floor = floor($rating);
		for ($i = 0, $array = []; $i < $length; $i++)
		{
			$array[] = $i < $floor ? 1 : 0;
		}
		if ($diff = $round - $floor)
		{
			$array[$floor] = $diff < 1 ? $diff : 1;
		}
		return $array;
	}
}
?>