using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; namespace FileManager.NamingTemplate; public class NamingTemplate { public string TemplateText { get; private set; } = string.Empty; public IEnumerable TagsInUse => _tagsInUse; public IEnumerable TagsRegistered => _tagCollections.SelectMany(t => t).DistinctBy(t => t.TagName); public IEnumerable Warnings => _errors.Concat(_warnings); public IEnumerable Errors => _errors; private Delegate? _templateToString; private readonly List _warnings = []; private readonly List _errors = []; private readonly List _tagCollections; private readonly List _tagsInUse = []; public const string ErrorNullIsInvalid = "Null template is invalid."; public const string WarningEmpty = "Template is empty."; public const string WarningWhiteSpace = "Template is white space."; public const string WarningNoTags = "Should use tags. Eg: "; /// <summary> /// Invoke the <see cref="NamingTemplate"/> /// </summary> /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param> public TemplatePart Evaluate(params object?[] propertyClasses) { if (_templateToString is null) throw new InvalidOperationException(); // Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). // First parameter is "this", so ignore it. var delegateArgTypes = _templateToString.Method.GetParameters().Skip(1).Select(p => p.ParameterType).ToList(); var delegateArgs = new object?[delegateArgTypes.Count]; var availableObjects = propertyClasses.Where(pc => pc is not null).Cast<object>().ToList(); for (var i = 0; i < delegateArgTypes.Count; i++) { var p = delegateArgTypes[i]; var index = availableObjects.FindIndex(pc => p.IsInstanceOfType(pc)); if (index < 0) { if (CanBeNull(p)) delegateArgs[i] = null; else throw new ArgumentException( $"No matching object found for parameter type {p.Name}. Available objects: {string.Join(", ", availableObjects.Select(o => o.GetType().Name))}"); } else { var candidate = availableObjects[index]; availableObjects.RemoveAt(index); availableObjects.Add(candidate); // Re-add to the end to allow reuse if needed later delegateArgs[i] = candidate; } } return (_templateToString.DynamicInvoke(delegateArgs) as TemplatePart)!.FirstPart; } /// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary> /// <param name="template">The template string to parse</param> /// <param name="tagCollections">A collection of <see cref="TagCollection"/> with /// properties registered to match to the <paramref name="template"/></param> public static NamingTemplate Parse(string? template, IEnumerable<TagCollection> tagCollections) { var listOfTagCollections = tagCollections.ToList(); var namingTemplate = new NamingTemplate(listOfTagCollections); try { var intermediate = namingTemplate.IntermediateParse(template); var evalTree = GetExpressionTree(intermediate); namingTemplate._templateToString = Expression.Lambda(evalTree, listOfTagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile(); } catch (Exception ex) { namingTemplate._errors.Add(ex.Message); } return namingTemplate; } private NamingTemplate(List<TagCollection> properties) { _tagCollections = properties; } /// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary> private static Expression GetExpressionTree(BinaryNode? node) { if (node is null) return TemplatePart.Blank; if (node.IsValue) return node.Expression; return node.IsConditional ? Expression.Condition(node.Expression, ConcatExpression(node), TemplatePart.Blank) : ConcatExpression(node); static Expression ConcatExpression(BinaryNode node) => TemplatePart.CreateConcatenation(GetExpressionTree(node.LeftChild), GetExpressionTree(node.RightChild)); } /// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary> private BinaryNode IntermediateParse(string? templateString) { if (templateString is null) throw new ArgumentException(ErrorNullIsInvalid); if (string.IsNullOrEmpty(templateString)) _warnings.Add(WarningEmpty); else if (string.IsNullOrWhiteSpace(templateString)) _warnings.Add(WarningWhiteSpace); TemplateText = templateString; var topNode = BinaryNode.CreateRoot(); var currentNode = topNode; List<char> literalChars = []; while (templateString.Length > 0) { if (StartsWith(templateString, OutputType.String, out var exactPropertyName, out var propertyTag, out var valueExpression)) { CheckAndAddLiterals(); if (propertyTag is IClosingPropertyTag) currentNode = currentNode.AddNewNode(BinaryNode.CreateConditional(propertyTag.TemplateTag, valueExpression)); else { currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(propertyTag.TemplateTag, valueExpression)); _tagsInUse.Add(propertyTag.TemplateTag); } templateString = templateString[exactPropertyName.Length..]; } else if (StartsWithClosing(templateString, out exactPropertyName, out var closingPropertyTag)) { CheckAndAddLiterals(); var lastParent = currentNode; while (lastParent?.IsConditional is false) lastParent = lastParent.Parent; if (lastParent?.Parent is null) { _warnings.Add($"Missing <{closingPropertyTag.TemplateTag.TagName}-> open conditional."); break; } else if (lastParent.Name != closingPropertyTag.TemplateTag.TagName) { _warnings.Add($"Missing <-{lastParent.Name}> closing conditional."); break; } currentNode = lastParent.Parent; templateString = templateString[exactPropertyName.Length..]; } else { //templateString does not start with a tag, so the first //character is a literal and not part of a tag expression. literalChars.Add(templateString[0]); templateString = templateString[1..]; } } CheckAndAddLiterals(); //Check for any conditionals that haven't been closed while (currentNode is not null) { if (currentNode.IsConditional) _warnings.Add($"Missing <-{currentNode.Name}> closing conditional."); currentNode = currentNode.Parent; } if (!_tagsInUse.Any()) _warnings.Add(WarningNoTags); return topNode; void CheckAndAddLiterals() { if (literalChars.Count != 0) { currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(string.Concat(literalChars))); literalChars.Clear(); } } } private bool StartsWith(string template, OutputType outputType, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression) { foreach (var pc in _tagCollections) { if (pc.StartsWith(template, outputType, out exactName, out propertyTag, out valueExpression)) return true; } exactName = null; valueExpression = null; propertyTag = null; return false; } private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag) { foreach (var pc in _tagCollections) { if (pc.StartsWithClosing(template, out exactName, out closingPropertyTag)) return true; } exactName = null; closingPropertyTag = null; return false; } private class BinaryNode { public string Name { get; } public BinaryNode? Parent { get; private set; } public BinaryNode? RightChild { get; private set; } public BinaryNode? LeftChild { get; private set; } public Expression Expression { get; } public bool IsConditional { get; private init; } public bool IsValue { get; private init; } public static BinaryNode CreateRoot() => new("Root", Expression.Empty()); public static BinaryNode CreateValue(string literal) => new("Literal", TemplatePart.CreateLiteral(literal)) { IsValue = true }; public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property)) { IsValue = true }; public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName, property) { IsConditional = true }; private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right) { var newNode = new BinaryNode("Concatenation", Expression.Empty()) { LeftChild = left, RightChild = right }; newNode.LeftChild.Parent = newNode; newNode.RightChild.Parent = newNode; return newNode; } private BinaryNode(string name, Expression expression) { Name = name; Expression = expression; } public override string ToString() => Name; public BinaryNode AddNewNode(BinaryNode newNode) { var currentNode = this; if (LeftChild is null) { newNode.Parent = currentNode; LeftChild = newNode; } else if (RightChild is null) { newNode.Parent = currentNode; RightChild = newNode; } else { RightChild = CreateConcatenation(RightChild, newNode); RightChild.Parent = currentNode; currentNode = RightChild; } return newNode.IsConditional ? newNode : currentNode; } } private static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) != null; }