using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using GFramework.Core.Abstractions.Logging;
using Godot;
namespace GFramework.Godot.Logging;
///
/// Writes Core instances to the Godot output APIs.
///
///
/// This appender is the Godot-specific edge of the Core logging pipeline. It keeps formatting, color selection, and
/// Godot debugger routing in the host package while allowing consumers to compose Godot output with Core
/// features such as CompositeLogger, filters, and async appenders. The appender
/// does not own unmanaged resources; and are therefore no-op lifecycle
/// hooks that satisfy the shared appender contract.
///
public sealed class GodotLogAppender : ILogAppender
{
private static readonly IReadOnlyDictionary EmptyProperties =
new Dictionary(StringComparer.Ordinal);
private readonly Func _optionsProvider;
///
/// Initializes a Godot appender with default Godot logger options.
///
public GodotLogAppender()
: this(new GodotLoggerOptions())
{
}
///
/// Initializes a Godot appender with fixed Godot logger options.
///
/// The formatting and routing options used for every appended entry.
/// is .
public GodotLogAppender(GodotLoggerOptions options)
: this(CreateFixedOptionsProvider(options))
{
}
///
/// Initializes a Godot appender with a dynamic options provider.
///
///
/// Provides the latest formatting and routing options for each append operation.
///
///
/// The Godot logger provider uses this constructor so cached loggers observe hot-reloaded settings without
/// being recreated. The provider must be fast and thread-safe because it is called on the logging path.
///
internal GodotLogAppender(Func optionsProvider)
{
_optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
}
///
/// Appends one Core log entry to Godot's console and debugger output.
///
/// The Core log entry to render.
/// is .
public void Append(LogEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
var options = _optionsProvider();
var rendered = Render(entry, options);
if (options.Mode == GodotLoggerMode.Debug)
{
WriteDebug(entry.Level, rendered);
}
else
{
GD.Print(rendered);
}
if (entry.Exception != null)
{
GD.PrintErr(entry.Exception.ToString());
}
}
///
/// Completes pending writes.
///
///
/// Godot output APIs are synchronous from this appender's point of view, so there is no buffered state to
/// flush.
///
public void Flush()
{
}
///
/// Releases appender resources.
///
///
/// The appender does not own disposable Godot resources. This method exists to honor the Core appender
/// lifecycle contract and to remain composable with factories that dispose appenders uniformly.
///
public void Dispose()
{
}
///
/// Formats structured properties for the {properties} template placeholder.
///
/// The already-merged property set from a Core .
///
/// A leading separator plus formatted properties, or an empty string when no valid properties exist.
///
///
/// Blank keys are ignored because they cannot produce useful structured output and can come from
/// caller-provided tuples. Valid keys are trimmed at render time so the appender never mutates the original
/// property dictionary.
///
internal static string FormatProperties(IReadOnlyDictionary? properties)
{
if (properties == null || properties.Count == 0)
{
return string.Empty;
}
var formattedProperties = properties
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
.Select(static pair => $"{pair.Key.Trim()}={FormatValue(pair.Value)}")
.ToArray();
return formattedProperties.Length == 0
? string.Empty
: " | " + string.Join(", ", formattedProperties);
}
///
/// Renders a Core log entry without writing it to Godot.
///
/// The Core log entry to render.
/// The line that would be sent to the selected Godot output API.
///
/// Tests use this method to verify template and structured-property behavior without depending on Godot's
/// static output APIs.
///
internal string Render(LogEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
return Render(entry, _optionsProvider());
}
private static Func CreateFixedOptionsProvider(GodotLoggerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
return () => options;
}
private static string Render(LogEntry entry, GodotLoggerOptions options)
{
var templateText = options.Mode == GodotLoggerMode.Debug
? options.DebugOutputTemplate
: options.ReleaseOutputTemplate;
var context = new GodotLogRenderContext(
entry.Timestamp,
entry.Level,
entry.LoggerName,
entry.Message,
options.GetColor(entry.Level),
FormatProperties(GetMergedProperties(entry)));
return GodotLogTemplate.Parse(templateText).Render(context);
}
private static IReadOnlyDictionary GetMergedProperties(LogEntry entry)
{
var allProperties = entry.GetAllProperties();
return allProperties.Count == 0 ? EmptyProperties : allProperties;
}
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;
}
}
}