namespace AIStudio.Tools.PowerPoint;
internal sealed partial class PresentationTemplate
{
///
/// Two layouts offer the same slots when every edge of every slot differs by no more than this
/// share of the slide.
///
private const double SLOT_TOLERANCE = 0.01;
///
/// Groups the layouts with room for content into families of variants.
///
///
/// Two layouts are variants of each other when they offer the same slots at the same places and
/// their names differ in one word only. Same slots alone are not enough: "Table 1" and "Table 2"
/// may well look alike to the reading code and still mean different things to the template's
/// author.
///
/// Templates put that word in front ("A Content 1", "B Content 1") or at the end ("Title blue",
/// "Title green"). Which one a template uses is decided for the whole template, by the reading
/// which finds more variants. Within a single template, a word in front and a word at the end
/// never mean the same thing.
///
private static GroupedLayouts GroupIntoFamilies(IReadOnlyList layouts, long slideWidth, long slideHeight)
{
var usableLayouts = layouts.Where(layout => layout.Slots.Any(slot => slot.Role.CarriesContent)).ToList();
var byFirstWord = Group(usableLayouts, VariantWord.FIRST, slideWidth, slideHeight);
var byLastWord = Group(usableLayouts, VariantWord.LAST, slideWidth, slideHeight);
var groups = CountVariants(byLastWord) > CountVariants(byFirstWord) ? byLastWord : byFirstWord;
var families = new List(groups.Count);
var variantLabels = new List();
foreach (var group in groups)
{
var id = $"L{families.Count + 1}";
if (group.Variants.Count == 1)
{
// Alone, the layout keeps its full name and needs no label:
families.Add(new LayoutFamily(id, group.Variants[0].Layout.Name, [new LayoutVariant(null, group.Variants[0].Layout)]));
continue;
}
families.Add(new LayoutFamily(id, group.Stem, group.Variants));
foreach (var variant in group.Variants)
if (variant.Label is { } label && !variantLabels.Contains(label))
variantLabels.Add(label);
}
return new GroupedLayouts(families, variantLabels);
}
private static List Group(IReadOnlyList layouts, VariantWord variantWord, long slideWidth, long slideHeight)
{
var candidates = new List();
foreach (var layout in layouts)
{
if (!TrySplitName(layout.Name, variantWord, out var label, out var stem))
{
candidates.Add(new FamilyCandidate(layout.Name, [new LayoutVariant(null, layout)]));
continue;
}
var family = candidates.FirstOrDefault(candidate =>
candidate.Stem == stem &&
candidate.Variants[0].Label is not null &&
candidate.Variants.All(variant => variant.Label != label) &&
HaveSameSlots(candidate.Variants[0].Layout, layout, slideWidth, slideHeight));
if (family is null)
candidates.Add(new FamilyCandidate(stem, [new LayoutVariant(label, layout)]));
else
family.Variants.Add(new LayoutVariant(label, layout));
}
return candidates;
}
private static bool TrySplitName(string name, VariantWord variantWord, out string label, out string stem)
{
var words = name.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (words.Length < 2)
{
label = string.Empty;
stem = string.Empty;
return false;
}
if (variantWord is VariantWord.FIRST)
{
label = words[0];
stem = string.Join(' ', words[1..]);
}
else
{
label = words[^1];
stem = string.Join(' ', words[..^1]);
}
return true;
}
private static bool HaveSameSlots(TemplateLayout first, TemplateLayout second, long slideWidth, long slideHeight)
{
if (first.Slots.Count != second.Slots.Count)
return false;
var horizontalTolerance = (long)(slideWidth * SLOT_TOLERANCE);
var verticalTolerance = (long)(slideHeight * SLOT_TOLERANCE);
return first.Slots.Zip(second.Slots).All(pair => pair.First.Role == pair.Second.Role && pair.First.Bounds.IsCloseTo(pair.Second.Bounds, horizontalTolerance, verticalTolerance));
}
///
/// How many layouts a grouping puts into families of more than one variant.
///
private static int CountVariants(IEnumerable candidates) => candidates.Where(candidate => candidate.Variants.Count > 1).Sum(candidate => candidate.Variants.Count);
///
/// Which word of a layout name tells the variants apart.
///
private enum VariantWord
{
FIRST,
LAST,
}
///
/// A family while it is being gathered.
///
/// The name without the word which tells the variants apart.
/// The variants found so far.
private sealed record FamilyCandidate(string Stem, List Variants);
///
/// The layouts with room for content, grouped into families of variants.
///
/// The families, in the order of the template.
/// The labels which tell the variants apart; empty when the template carries every layout once.
private sealed record GroupedLayouts(IReadOnlyList Families, IReadOnlyList VariantLabels);
}