using System; using System.Collections.Generic; using System.Linq; using System.Reflection; // Required for Reflection namespace SessionZero.Data; public class SzfParser { // A static map to cache SzfObject types by their TypeIdentifier private static readonly Dictionary _szfObjectTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly object _typeMapLock = new object(); // For thread safety during map population public SzfParser() { // Ensure the type map is populated when a parser instance is created EnsureTypeMapPopulated(); } /// /// Ensures the internal map of SzfObject types and their string identifiers is populated. /// This uses reflection to find all concrete SzfObject classes with the SzfObjectAttribute. /// private static void EnsureTypeMapPopulated() { if (_szfObjectTypeMap.Any()) return; // Already populated lock (_typeMapLock) { if (_szfObjectTypeMap.Any()) return; // Double-check after acquiring lock // Find all concrete (non-abstract) types that inherit from SzfObject // and have the SzfObjectAttribute defined. var szfObjectTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(s => s.GetTypes()) .Where(p => typeof(SzfObject).IsAssignableFrom(p) && !p.IsAbstract && p.IsDefined(typeof(SzfObjectAttribute), false)); foreach (var type in szfObjectTypes) { var attribute = type.GetCustomAttribute(); if (attribute != null) { // Add to the map using the TypeIdentifier from the attribute _szfObjectTypeMap[attribute.TypeIdentifier] = type; } } } } /// /// Parses an SZF content string into a specific SzfObject type. /// /// The expected type of SzfObject. /// The SZF content string. /// An instance of the parsed SzfObject. /// Thrown if parsing fails or types mismatch. public T Parse(string szfContent) where T : SzfObject { var parseResult = ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList()); // Validate that the parsed type identifier matches the expected type T ValidateExpectedType(parseResult.ObjectTypeIdentifier); // Create an instance of the specific SzfObject type using the parsed identifier var obj = CreateInstance(parseResult.ObjectTypeIdentifier); PopulateSzfObject(obj, parseResult); return obj; } /// /// Parses an SZF content string into a generic SzfObject. The concrete type will be determined from the content. /// /// The SZF content string. /// An instance of the parsed SzfObject. /// Thrown if parsing fails. public SzfObject Parse(string szfContent) { var parseResult = ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList()); // Create an instance of the specific SzfObject type using the parsed identifier var obj = CreateInstanceFromTypeIdentifier(parseResult.ObjectTypeIdentifier); PopulateSzfObject(obj, parseResult); return obj; } /// /// Creates an instance of the specified SzfObject type, ensuring it's assignable to T. /// /// The type to cast the created instance to. /// The string identifier of the SzfObject type. /// A new instance of the SzfObject. /// Thrown if the type is unknown or cannot be instantiated. private T CreateInstance(string typeIdentifier) where T : SzfObject { EnsureTypeMapPopulated(); if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type)) { throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'."); } if (!typeof(T).IsAssignableFrom(type)) { throw new SzfParseException( $"Requested type '{typeof(T).Name}' is not assignable from parsed type '{type.Name}'."); } // Use Activator.CreateInstance to create an instance of the found type return (T)Activator.CreateInstance(type)!; } /// /// Creates a generic SzfObject instance from its string identifier. /// /// The string identifier of the SzfObject type. /// A new instance of SzfObject. /// Thrown if the type is unknown or cannot be instantiated. public SzfObject CreateInstanceFromTypeIdentifier(string typeIdentifier) { EnsureTypeMapPopulated(); if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type)) { throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'."); } return (SzfObject)Activator.CreateInstance(type)!; } /// /// Parses the raw SZF content lines into a structured SzfParseResult. /// public SzfParseResult ParseSzfStructure(List lines) { var result = new SzfParseResult(); var currentSectionLines = new List(); string? currentSectionName = null; foreach (var line in lines) { var trimmedLine = line.Trim(); // Handle headers (!type:, !schema:) if (trimmedLine.StartsWith("!type:", StringComparison.OrdinalIgnoreCase)) { result.ObjectTypeIdentifier = trimmedLine.Substring("!type:".Length).Trim(); continue; } if (trimmedLine.StartsWith("!schema:", StringComparison.OrdinalIgnoreCase)) { result.SchemaVersion = trimmedLine.Substring("!schema:".Length).Trim(); continue; } if (trimmedLine.StartsWith("!", StringComparison.Ordinal)) // Other !headers { var parts = trimmedLine.Split(new[] { ':' }, 2); if (parts.Length == 2) { result.Headers[parts[0].TrimStart('!').Trim()] = parts[1].Trim(); } continue; } // Handle section headers ([Section Name]) if (trimmedLine.StartsWith("[") && trimmedLine.EndsWith("]")) { // If there's an active section, process it before starting a new one if (currentSectionName != null) { ProcessSection(result, currentSectionName, currentSectionLines); currentSectionLines.Clear(); } currentSectionName = trimmedLine.Substring(1, trimmedLine.Length - 2).Trim(); continue; } // Handle empty lines (ignored within sections for parsing structure) if (string.IsNullOrWhiteSpace(trimmedLine)) { continue; } // Otherwise, it's a field line within the current section if (currentSectionName != null) { currentSectionLines.Add(trimmedLine); } else { // Fields found before any section header, treat as global/unassigned, or throw error // For now, we'll ignore them or consider them an error // You might want to throw an exception here depending on strictness } } // Process the last section if any if (currentSectionName != null) { ProcessSection(result, currentSectionName, currentSectionLines); } return result; } /// /// Processes lines belonging to a single section, parsing fields and adding to the result. /// private void ProcessSection(SzfParseResult result, string sectionName, List sectionLines) { var szfSection = new SzfSection { Name = sectionName }; foreach (var line in sectionLines) { // Expected format: Name (type) = Value var parts = line.Split(new[] { '=' }, 2); if (parts.Length == 2) { var nameAndTypePart = parts[0].Trim(); var valueString = parts[1].Trim(); string fieldName; string fieldType = "text"; // Default type if not specified // Check for (type) in name part var typeStart = nameAndTypePart.LastIndexOf('('); var typeEnd = nameAndTypePart.LastIndexOf(')'); if (typeStart != -1 && typeEnd != -1 && typeEnd > typeStart) { fieldName = nameAndTypePart.Substring(0, typeStart).Trim(); fieldType = nameAndTypePart.Substring(typeStart + 1, typeEnd - typeStart - 1).Trim(); } else { fieldName = nameAndTypePart; } szfSection.Fields.Add(new SzfField { Name = fieldName, Type = fieldType, Value = ParseFieldValue(valueString, fieldType) }); } // Else, malformed field line, ignore or log error } result.Sections.Add(szfSection); } /// /// Parses a field value string into an appropriate object type based on the field type. /// private object? ParseFieldValue(string valueString, string fieldType) { return fieldType.ToLowerInvariant() switch { "number" => ParseNumber(valueString), "bool" => ParseBoolean(valueString), "text-field" => valueString, // Text-fields are multi-line, keep as string _ => valueString // Default to string for "text" and unknown types }; } private object ParseNumber(string value) { if (int.TryParse(value, out var i)) return i; if (double.TryParse(value, out var d)) return d; return 0; // Default or throw error } private bool ParseBoolean(string value) { return bool.TryParse(value, out var b) && b; } /// /// Validates that the parsed object type identifier matches the expected type T. /// /// The expected SzfObject type. /// The string identifier parsed from the SZF content. /// Thrown if the types do not match. /// Thrown if T is not properly decorated with SzfObjectAttribute. private void ValidateExpectedType(string parsedTypeIdentifier) where T : SzfObject { var targetAttribute = typeof(T).GetCustomAttribute(); if (targetAttribute == null) { throw new InvalidOperationException( $"Type '{typeof(T).Name}' is missing the SzfObjectAttribute. Cannot validate expected type."); } if (!string.Equals(targetAttribute.TypeIdentifier, parsedTypeIdentifier, StringComparison.OrdinalIgnoreCase)) { throw new SzfParseException( $"Mismatched SzfObject type. Expected '{targetAttribute.TypeIdentifier}' but found '{parsedTypeIdentifier}'."); } } /// /// Populates an SzfObject instance with the data from a parsed SzfParseResult. /// public void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult) { // Extract metadata fields into a dictionary for PopulateFromMetadata var metadataFields = parseResult.Sections .FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))? .Fields.ToDictionary(f => f.Name, f => f, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(); obj.PopulateFromMetadata(metadataFields); // Pass all sections (excluding Metadata, which PopulateFromMetadata might handle) to ParseSections // ParseSections is responsible for building the object's specific internal structure. obj.ParseSections(parseResult.Sections); } } /// /// Represents the parsed structure of an SZF file. /// public class SzfParseResult { // Changed from SzfObjectType to string public string ObjectTypeIdentifier { get; set; } = string.Empty; public string SchemaVersion { get; set; } = string.Empty; public Dictionary Headers { get; set; } = new Dictionary(); public List Sections { get; set; } = new List(); } /// /// Represents a section within an SZF file. /// public class SzfSection { public string Name { get; set; } = string.Empty; public List Fields { get; set; } = new List(); } /// /// Exception thrown during SZF parsing. /// public class SzfParseException : Exception { public SzfParseException(string message) : base(message) { } public SzfParseException(string message, Exception innerException) : base(message, innerException) { } }