using PlotLine.Models; namespace PlotLine.Services; public sealed class CharacterAgeDisplayResult { public bool HasExactAge { get; init; } public string? Label { get; init; } public string? DisplayText { get; init; } } public sealed record CharacterProfileAgeDisplayResult(string DisplayText, string Context, Scene? ReferenceScene); public static class CharacterAgeDisplayService { public static CharacterProfileAgeDisplayResult GetProfileAge(Character character, IReadOnlyList orderedScenes, IReadOnlyList appearances) { var appearanceIds = appearances.Where(x => x.CharacterID == character.CharacterID).Select(x => x.SceneID).ToHashSet(); var reference = character.AgeReferenceSceneID.HasValue ? orderedScenes.FirstOrDefault(x => x.SceneID == character.AgeReferenceSceneID) : null; if (reference is null || !ResolveEffectiveSceneDate(reference.TimeModeName, reference.StartDateTime).HasValue) reference = orderedScenes.FirstOrDefault(x => appearanceIds.Contains(x.SceneID) && ResolveEffectiveSceneDate(x.TimeModeName, x.StartDateTime).HasValue && (!character.BirthDate.HasValue || x.StartDateTime!.Value.Date >= character.BirthDate.Value.Date)); var age = GetAgeForScene(character, reference?.TimeModeName, reference?.StartDateTime); return new CharacterProfileAgeDisplayResult(age.DisplayText ?? "Unknown", age.HasExactAge ? (reference?.SceneID == character.AgeReferenceSceneID ? "At age reference scene" : "At first dated appearance") : character.AgeAtSeriesStart.HasValue ? "Approximate age at series start" : character.BirthDate.HasValue ? "A dated story scene is needed to calculate age." : "No birth date or approximate age recorded.", age.HasExactAge ? reference : null); } public static CharacterAgeDisplayResult GetAgeForScene(SceneCharacter character, string? sceneTimeModeName, DateTime? sceneStartDateTime) { var effectiveDate = ResolveEffectiveSceneDate(sceneTimeModeName, sceneStartDateTime); if (character.BirthDate.HasValue && effectiveDate.HasValue) { return new CharacterAgeDisplayResult { HasExactAge = true, Label = "Age in scene", DisplayText = FormatExactAge(character.BirthDate.Value.Date, effectiveDate.Value.Date) }; } if (character.AgeAtSeriesStart.HasValue) { return new CharacterAgeDisplayResult { Label = "Approximate age", DisplayText = $"About {character.AgeAtSeriesStart.Value}" }; } return new CharacterAgeDisplayResult(); } public static CharacterAgeDisplayResult GetAgeForScene(Character character, string? sceneTimeModeName, DateTime? sceneStartDateTime) { var effectiveDate = ResolveEffectiveSceneDate(sceneTimeModeName, sceneStartDateTime); if (character.BirthDate.HasValue && effectiveDate.HasValue) { return new CharacterAgeDisplayResult { HasExactAge = true, Label = "Age in scene", DisplayText = FormatExactAge(character.BirthDate.Value.Date, effectiveDate.Value.Date) }; } if (character.AgeAtSeriesStart.HasValue) { return new CharacterAgeDisplayResult { Label = "Approximate age", DisplayText = $"About {character.AgeAtSeriesStart.Value}" }; } return new CharacterAgeDisplayResult(); } public static DateTime? ResolveEffectiveSceneDate(string? timeModeName, DateTime? startDateTime) { if (!startDateTime.HasValue) { return null; } return string.Equals(timeModeName, "Exact Date", StringComparison.OrdinalIgnoreCase) || string.Equals(timeModeName, "Exact DateTime", StringComparison.OrdinalIgnoreCase) || string.Equals(timeModeName, "Exact Date and Time", StringComparison.OrdinalIgnoreCase) ? startDateTime.Value.Date : null; } public static string FormatExactAge(DateTime birthDate, DateTime sceneDate) { var (years, months) = CalculateExactAge(birthDate, sceneDate); return months == 0 ? $"{years} year{Pluralise(years)}" : $"{years} year{Pluralise(years)}, {months} month{Pluralise(months)}"; } public static (int Years, int Months) CalculateExactAge(DateTime birthDate, DateTime sceneDate) { var years = sceneDate.Year - birthDate.Year; if (sceneDate < birthDate.AddYears(years)) { years--; } if (years < 0) { years = 0; } var lastBirthday = birthDate.AddYears(years); var months = ((sceneDate.Year - lastBirthday.Year) * 12) + sceneDate.Month - lastBirthday.Month; if (sceneDate.Day < lastBirthday.Day) { months--; } if (months < 0) { months = 0; } return (years, months); } private static string Pluralise(int value) => value == 1 ? string.Empty : "s"; }