using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; namespace TwitchChatTTS.Chat.Commands { public static class TTSCommands { public interface ICommandBuilder { ICommandSelector Build(); void Clear(); ICommandBuilder CreateCommandTree(string name, Action callback); ICommandBuilder CreateCommand(IChatPartialCommand command); ICommandBuilder CreateStaticInputParameter(string value, Action callback, bool optional = false); ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false); ICommandBuilder CreateStateParameter(string name, bool optional = false); ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false); ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false); } public sealed class CommandBuilder : ICommandBuilder { private CommandNode _root; private CommandNode _current; private Stack _stack; private readonly User _user; private readonly ILogger _logger; public CommandBuilder(User user, ILogger logger) { _user = user; _logger = logger; _stack = new Stack(); Clear(); } public ICommandSelector Build() { return new CommandSelector(_root); } public void Clear() { _root = new CommandNode(new StaticParameter("root", "root")); ResetToRoot(); } public ICommandBuilder CreateCommandTree(string name, Action callback) { ResetToRoot(); var node = _current.CreateStaticInput(name); _logger.Debug($"Creating command name '{name}'"); CreateStack(() => { _current = node; callback(this); }); return this; } public ICommandBuilder CreateCommand(IChatPartialCommand command) { if (_root == _current) throw new Exception("Cannot create a command without a command name."); _current.CreateCommand(command); _logger.Debug($"Set command to '{command.GetType().Name}'"); return this; } public ICommandBuilder CreateStaticInputParameter(string value, Action callback, bool optional = false) { if (_root == _current) throw new Exception("Cannot create a parameter without a command name."); if (optional && _current.IsRequired() && _current.Command == null) throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); var node = _current.CreateStaticInput(value, optional); _logger.Debug($"Creating static parameter '{value}'"); CreateStack(() => { _current = node; callback(this); }); return this; } public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false) { if (_root == _current) throw new Exception("Cannot create a parameter without a command name."); if (optional && _current.IsRequired() && _current.Command == null) throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); var node = _current.CreateUserInput(new OBSTransformationParameter(name, optional)); _logger.Debug($"Creating obs transformation parameter '{name}'"); _current = node; return this; } public ICommandBuilder CreateStateParameter(string name, bool optional = false) { if (_root == _current) throw new Exception("Cannot create a parameter without a command name."); if (optional && _current.IsRequired() && _current.Command == null) throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); var node = _current.CreateUserInput(new StateParameter(name, optional)); _logger.Debug($"Creating unvalidated parameter '{name}'"); _current = node; return this; } public ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false) { if (_root == _current) throw new Exception("Cannot create a parameter without a command name."); if (optional && _current.IsRequired() && _current.Command == null) throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); var node = _current.CreateUserInput(new UnvalidatedParameter(name, optional)); _logger.Debug($"Creating unvalidated parameter '{name}'"); _current = node; return this; } public ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false) { if (_root == _current) throw new Exception("Cannot create a parameter without a command name."); if (optional && _current.IsRequired() && _current.Command == null) throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); var node = _current.CreateUserInput(new TTSVoiceNameParameter(name, enabled, _user, optional)); _logger.Debug($"Creating tts voice name parameter '{name}'"); _current = node; return this; } private ICommandBuilder ResetToRoot() { _current = _root; _stack.Clear(); return this; } private void CreateStack(Action func) { try { _stack.Push(_current); func(); } finally { _current = _stack.Pop(); } } } public interface ICommandSelector { CommandSelectorResult GetBestMatch(string[] args); IDictionary GetNonStaticArguments(string[] args, string path); CommandValidationResult Validate(string[] args, string path); } public sealed class CommandSelector : ICommandSelector { private CommandNode _root; public CommandSelector(CommandNode root) { _root = root; } public CommandSelectorResult GetBestMatch(string[] args) { return GetBestMatch(_root, args, null, string.Empty); } private CommandSelectorResult GetBestMatch(CommandNode node, IEnumerable args, IChatPartialCommand? match, string path) { if (node == null || !args.Any()) return new CommandSelectorResult(match, path); if (!node.Children.Any()) return new CommandSelectorResult(node.Command ?? match, path); var argument = args.First(); var argumentLower = argument.ToLower(); foreach (var child in node.Children) { if (child.Parameter.GetType() == typeof(StaticParameter)) { if (child.Parameter.Name.ToLower() == argumentLower) { return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower()); } continue; } return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + "*"); } return new CommandSelectorResult(match, path); } public CommandValidationResult Validate(string[] args, string path) { CommandNode? current = _root; var parts = path.Split('.'); if (args.Length < parts.Length) throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]"); for (var i = 0; i < parts.Length; i++) { var part = parts[i]; if (part == "*") { current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter)); if (current == null) throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); if (!current.Parameter.Validate(args[i])) { return new CommandValidationResult(false, args[i]); } } else { current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part); if (current == null) throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); } } return new CommandValidationResult(true, null); } public IDictionary GetNonStaticArguments(string[] args, string path) { Dictionary arguments = new Dictionary(); CommandNode? current = _root; var parts = path.Split('.'); if (args.Length < parts.Length) throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]"); for (var i = 0; i < parts.Length; i++) { var part = parts[i]; if (part == "*") { current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter)); if (current == null) throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); arguments.Add(args[i], current.Parameter); } else { current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part); if (current == null) throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); } } return arguments; } } public class CommandSelectorResult { public IChatPartialCommand? Command { get; set; } public string Path { get; set; } public CommandSelectorResult(IChatPartialCommand? command, string path) { Command = command; Path = path; } } public class CommandValidationResult { public bool Result { get; set; } public string? ErrorParameterName { get; set; } public CommandValidationResult(bool result, string? parameterName) { Result = result; ErrorParameterName = parameterName; } } public sealed class CommandNode { public IChatPartialCommand? Command { get; private set; } public CommandParameter Parameter { get; } public IList Children { get => _children.AsReadOnly(); } private IList _children; public CommandNode(CommandParameter parameter) { Parameter = parameter; _children = new List(); } public CommandNode CreateCommand(IChatPartialCommand command) { if (Command != null) throw new InvalidOperationException("Cannot change the command of an existing one."); Command = command; return this; } public CommandNode CreateStaticInput(string value, bool optional = false) { if (Children.Any(n => n.Parameter.GetType() != typeof(StaticParameter))) throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand."); return Create(n => n.Parameter.Name == value, new StaticParameter(value.ToLower(), value, optional)); } public CommandNode CreateUserInput(CommandParameter parameter) { if (Children.Any(n => n.Parameter.GetType() == typeof(StaticParameter))) throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand."); return Create(n => true, parameter); } private CommandNode Create(Predicate predicate, CommandParameter parameter) { CommandNode? node = Children.FirstOrDefault(n => predicate(n)); if (node == null) { node = new CommandNode(parameter); _children.Add(node); } if (node.Parameter.GetType() != parameter.GetType()) throw new Exception("User input argument already exist for this partial command."); return node; } public bool IsRequired() { return !Parameter.Optional; } } } }