using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; using Godot; namespace GFramework.Godot.Logging; /// /// Godot platform logger implementation. /// public sealed class GodotLogger : AbstractLogger { private readonly Func _optionsProvider; /// /// Initializes a logger that preserves the historical fixed-format template. /// /// The logger name. /// The minimum enabled log level. public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : this( name ?? RootLoggerName, CreateFixedOptionsProvider(minLevel), () => minLevel) { } /// /// Initializes a logger with Godot-specific formatting options. /// /// The logger name. /// The logger options. public GodotLogger(string? name, GodotLoggerOptions options) : this( name ?? RootLoggerName, CreateOptionsProvider(options), CreateMinLevelProvider(options)) { } /// /// Initializes the core logger with dynamic options and level providers. /// /// The resolved logger name used in rendered output. /// The provider that supplies the latest rendering options for each write. /// The provider that supplies the latest effective minimum level. /// /// The Godot factory uses this constructor so cached logger instances can observe hot-reloaded settings without /// being recreated. The default public constructor supplies a fixed provider to avoid allocation on the log path. /// internal GodotLogger( string name, Func optionsProvider, Func minLevelProvider) : base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider))) { _optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider)); } /// /// Writes a log entry to Godot. /// /// The log level. /// The rendered message body. /// The optional exception. protected override void Write(LogLevel level, string message, Exception? exception) { WriteEntry(level, message, exception, properties: null); } /// /// Uses Godot-aware structured rendering instead of the base string concatenation fallback. /// public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties) { if (!IsEnabled(level)) { return; } WriteEntry(level, message, exception: null, properties); } /// /// Uses Godot-aware structured rendering instead of the base string concatenation fallback. /// public override void Log( LogLevel level, string message, Exception? exception, params (string Key, object? Value)[] properties) { if (!IsEnabled(level)) { return; } WriteEntry(level, message, exception, properties); } private void WriteEntry( LogLevel level, string message, Exception? exception, (string Key, object? Value)[]? properties) { var options = _optionsProvider(); var templateText = options.Mode == GodotLoggerMode.Debug ? options.DebugOutputTemplate : options.ReleaseOutputTemplate; var context = new GodotLogRenderContext( DateTime.UtcNow, level, Name(), message, options.GetColor(level), FormatProperties(properties)); var rendered = GodotLogTemplate.Parse(templateText).Render(context); if (options.Mode == GodotLoggerMode.Debug) { WriteDebug(level, rendered); } else { GD.Print(rendered); } if (exception != null) { GD.PrintErr(exception.ToString()); } } private static string FormatProperties((string Key, object? Value)[]? properties) { var merged = MergeProperties(properties); if (merged.Count == 0) { return string.Empty; } return " | " + string.Join(", ", merged.Select(static pair => $"{pair.Key}={FormatValue(pair.Value)}")); } private static IReadOnlyDictionary MergeProperties((string Key, object? Value)[]? properties) { var contextProperties = LogContext.Current; if ((properties == null || properties.Length == 0) && contextProperties.Count == 0) { return EmptyProperties; } var merged = new Dictionary(contextProperties, StringComparer.Ordinal); if (properties != null) { foreach (var property in properties) { if (string.IsNullOrWhiteSpace(property.Key)) { continue; } merged[property.Key.Trim()] = property.Value; } } return merged; } private static readonly IReadOnlyDictionary EmptyProperties = new Dictionary(StringComparer.Ordinal); private static Func CreateFixedOptionsProvider(LogLevel minLevel) { var options = GodotLoggerOptions.ForMinimumLevel(minLevel); return () => options; } private static Func CreateOptionsProvider(GodotLoggerOptions options) { ArgumentNullException.ThrowIfNull(options); return () => options; } private static Func CreateMinLevelProvider(GodotLoggerOptions options) { ArgumentNullException.ThrowIfNull(options); return () => options.GetEffectiveMinLevel(); } private static string FormatValue(object? value) { if (value == null) { return "null"; } return value switch { IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), _ => value.ToString() ?? string.Empty }; } private static void WriteDebug(LogLevel level, string rendered) { GD.PrintRich(rendered); switch (level) { case LogLevel.Fatal: case LogLevel.Error: GD.PushError(rendered); break; case LogLevel.Warning: GD.PushWarning(rendered); break; } } }