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; } }