using MaBibli.Shared.Dtos;
using MaBibli.Shared.Textes;
namespace MaBibli.Client.Services;
///
/// Une entrée du catalogue : un livre seul, ou les tomes d'une même série sous son nom.
///
public sealed record EntreeCatalogue
{
/// La série qui coiffe le bloc, ou null pour un livre seul.
public SerieDto? Serie { get; init; }
/// Les livres de l'entrée : un seul hors série, les tomes visibles sinon.
public required IReadOnlyList Livres { get; init; }
public bool EstGroupe => Serie is not null;
}
///
/// Regroupe les livres du catalogue par série.
///
///
/// Le bloc se place là où son premier tome serait tombé dans l'ordre du catalogue
/// (choisi avec l'utilisateur le 2026-08-22) : l'ordre général reste celui qu'on connaît, et
/// l'on retrouve « La Légende de Drizzt » entre « Dracula » et « Dune ». À l'intérieur, les
/// tomes suivent l'ordre de lecture, seul ordre qui ait un sens pour une saga — c'est
/// même la raison d'être de ElementSerie.Position.
///
/// ⚠️ Rien n'est jamais masqué ni déplacé hors de la liste. Un livre filtré reste absent,
/// un livre visible reste visible : le regroupement ne fait que rassembler, et l'écran offre de
/// le défaire. C'est la même règle que le grisage de la bibliographie — on marque, on ne cache
/// pas.
///
///
/// ⚠️ Un livre peut appartenir à plusieurs séries (le modèle l'autorise, sans unicité sur
/// LivreId seul). Il n'apparaît pourtant qu'une fois : le dupliquer ferait mentir le
/// compteur du catalogue et donnerait deux cartes du même exemplaire. La série retenue est la
/// première par ordre alphabétique — un critère explicable, à défaut d'être le bon dans
/// tous les cas ; sa fiche livre, elle, les montre toutes.
///
///
public static class GroupementCatalogue
{
public static IReadOnlyList Grouper(
IReadOnlyList livres, IReadOnlyList? series)
{
if (series is null || series.Count == 0)
{
return [.. livres.Select(l => new EntreeCatalogue { Livres = [l] })];
}
var place = PlaceDesLivres(series);
// Les tomes s'accumulent dans le brouillon du groupe, créé à la position de son PREMIER
// tome rencontré : c'est ce qui range le bloc là où l'ordre du catalogue l'attend.
var brouillons = new List<(SerieDto? Serie, List<(int Position, LivreDto Livre)> Livres)>();
var groupes = new Dictionary>();
foreach (var livre in livres)
{
if (!place.TryGetValue(livre.Id, out var appartenance))
{
brouillons.Add((null, [(0, livre)]));
continue;
}
if (!groupes.TryGetValue(appartenance.Serie.Id, out var tomes))
{
tomes = [];
groupes[appartenance.Serie.Id] = tomes;
brouillons.Add((appartenance.Serie, tomes));
}
tomes.Add((appartenance.Position, livre));
}
return
[
.. brouillons.Select(b => new EntreeCatalogue
{
Serie = b.Serie,
Livres =
[
.. b.Livres
.OrderBy(t => t.Position)
.ThenBy(t => t.Livre.Id)
.Select(t => t.Livre),
],
}),
];
}
///
/// À quelle série — et à quelle place — appartient chaque livre rattaché.
///
///
/// ⚠️ Les séries sont parcourues dans l'ordre alphabétique de leur titre normalisé, et le
/// TryAdd garde donc la première : sans cet ordre, la série retenue pour un
/// livre rattaché deux fois dépendrait de l'ordre où l'API rend les séries, c'est-à-dire de
/// rien de compréhensible.
///
private static Dictionary PlaceDesLivres(
IReadOnlyList series)
{
var place = new Dictionary();
foreach (var serie in series
.OrderBy(s => NormalisationTexte.Normaliser(s.Titre), StringComparer.Ordinal)
.ThenBy(s => s.Id))
{
foreach (var element in serie.Elements)
{
if (element.LivreId is { } livreId)
{
place.TryAdd(livreId, (serie, element.Position));
}
}
}
return place;
}
}