using System.IO;
using System.Linq;
using System.Text;
namespace DragonISO.App.Services;
///
/// User-editable template for the NDI source name a participant's ISO is
/// published as. Default "{name}" renders the speaker's display name
/// directly, which is what downstream switchers want when they key on
/// readable identifiers. Operators can override globally to
/// "Dragon-ISO_{guid}" for the legacy stable-id behavior, or
/// "Dragon-ISO_{machine}_{name}" when multiple Dragon-ISO machines feed
/// the same NDI network and you want the source name to carry both.
/// Per-participant overrides take priority over whatever template is set.
///
/// Tokens expanded in :
/// {name} participant display name, sanitized (alphanumeric + underscore)
/// {guid} first 8 hex chars of the participant's Id, uppercase
/// {machine} sanitized PC hostname (Environment.MachineName)
/// {timestamp} current local time as yyyyMMdd_HHmmss
///
/// Empty-name fallback: if the rendered result is empty/whitespace (e.g.
/// template was "{name}" and the participant joined with no display
/// name yet), falls back to Dragon-ISO_{guid} so
/// the NDI sender always has a usable, unique identifier.
///
/// Persisted to %LOCALAPPDATA%\Dragon-ISO\output-name-template.txt.
///
public static class OutputNameTemplate
{
///
/// Default template — renders just the speaker's display name. Was
/// "Dragon-ISO_{guid}" in pre-v1 builds; switched 2026-05-16 so
/// new installs get human-readable source names out of the box.
///
public const string DefaultTemplate = "{name}";
///
/// Stable fallback used when the rendered template produces an empty
/// string (typically because a participant has no display name yet).
/// Mirrors the engine's legacy DefaultOutputName so the NDI sender is
/// always uniquely identifiable.
///
private const string EmptyNameFallback = "Dragon-ISO_{guid}";
private static string TemplatePath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Dragon-ISO", "output-name-template.txt");
///
/// Get the operator's current template, or the shipped default when no
/// override has been saved (or the override file is missing/unreadable).
///
public static string Get()
{
try
{
if (File.Exists(TemplatePath))
{
var raw = File.ReadAllText(TemplatePath).Trim();
if (!string.IsNullOrEmpty(raw)) return raw;
}
}
catch
{
// Disk read failure → fall through to default. The next Set() call
// will overwrite cleanly.
}
return DefaultTemplate;
}
public static void Set(string template)
{
try
{
var dir = Path.GetDirectoryName(TemplatePath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(TemplatePath, template ?? string.Empty);
}
catch
{
// Best-effort persistence; the in-memory value still sticks for
// this session.
}
}
///
/// Expand tokens in for a specific participant.
/// Result is sanitized into NDI-safe characters: alphanumeric, underscore,
/// hyphen, period. NDI spec allows more, but a conservative set keeps
/// downstream switchers happy.
///
public static string Render(string template, Guid participantId, string displayName)
{
var safeName = SanitizeForNdi(displayName);
var guid = participantId.ToString("N")[..8].ToUpperInvariant();
var machine = SanitizeForNdi(Environment.MachineName);
var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd_HHmmss");
var result = template
.Replace("{name}", safeName)
.Replace("{guid}", guid)
.Replace("{machine}", machine)
.Replace("{timestamp}", timestamp);
// Final sanitize on the rendered result — protects against a template
// that includes literal characters NDI doesn't accept.
var sanitized = SanitizeForNdi(result);
// Empty-name fallback. The default template "{name}" can render to
// an unusable result for participants whose DisplayName hasn't been
// populated yet (Teams sometimes delivers the displayName a tick
// after the participant join event). Two failure modes to catch:
//
// • DisplayName == "" → "{name}" expands to "" → sanitized "".
// • DisplayName == " " → "{name}" expands to "___" because the
// sanitizer converts whitespace to underscores.
//
// Neither is a meaningful NDI source identifier, so we substitute
// Dragon-ISO_{guid}. The Any(char.IsLetterOrDigit) check covers both
// cases — anything without at least one alphanumeric is unusable.
// We apply this AFTER token expansion (not on the raw input) so a
// template like "PFX_{name}" with empty displayName still works:
// it renders to "PFX_" which contains alphanumerics and is left
// alone.
if (string.IsNullOrWhiteSpace(sanitized) || !sanitized.Any(char.IsLetterOrDigit))
{
sanitized = SanitizeForNdi(EmptyNameFallback.Replace("{guid}", guid));
}
return sanitized;
}
private static string SanitizeForNdi(string s)
{
if (string.IsNullOrEmpty(s)) return string.Empty;
var sb = new StringBuilder(s.Length);
foreach (var c in s)
{
if (char.IsLetterOrDigit(c) || c is '_' or '-' or '.')
sb.Append(c);
else if (char.IsWhiteSpace(c))
sb.Append('_');
// else: skip
}
return sb.ToString();
}
}