// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using GFramework.Core.Abstractions.Logging; namespace GFramework.Godot.Logging; /// /// Represents one immutable Godot logger configuration snapshot. /// /// /// A snapshot combines mode-specific , an optional default log level, and category overrides. /// Category matching is ordinal and deterministic: exact matches win first, then the longest dotted prefix such as /// Game.Services for Game.Services.Inventory, and finally is used when /// present. /// internal sealed class GodotLoggerSettings { private readonly IReadOnlyDictionary _loggerLevels; /// /// Gets the default settings snapshot used when no configuration file is available. /// public static GodotLoggerSettings Default { get; } = new(new GodotLoggerOptions()); /// /// Creates a settings snapshot from normalized options and optional category thresholds. /// /// The formatting and mode options for this snapshot. /// The optional fallback level used when no category override matches. /// Exact category names or dotted prefixes mapped to minimum levels. public GodotLoggerSettings( GodotLoggerOptions options, LogLevel? defaultLogLevel = null, IReadOnlyDictionary? loggerLevels = null) { Options = (options ?? throw new ArgumentNullException(nameof(options))).CreateNormalizedCopy(); DefaultLogLevel = defaultLogLevel; _loggerLevels = loggerLevels ?? new Dictionary(StringComparer.Ordinal); } /// /// Gets the optional fallback minimum level for categories without exact or prefix overrides. /// public LogLevel? DefaultLogLevel { get; } /// /// Gets normalized rendering and mode options for this snapshot. /// public GodotLoggerOptions Options { get; } /// /// Gets exact and dotted-prefix category level overrides. /// /// /// Keys are interpreted with semantics. A key only matches a child category /// when the category starts with the key plus a dot, which prevents Game.Service from matching /// Game.Services accidentally. /// public IReadOnlyDictionary LoggerLevels => _loggerLevels; /// /// Creates a settings snapshot from options without any category overrides. /// /// The options to normalize and wrap. /// A settings snapshot that relies only on the option-level minimum level. public static GodotLoggerSettings FromOptions(GodotLoggerOptions options) { return new GodotLoggerSettings(options); } /// /// Calculates the effective minimum level for a category. /// /// The logger category name. /// The provider-level floor captured by the logger. /// The strictest level selected from options, provider floor, and category configuration. /// /// The merge starts with and /// , then applies when it returns a /// value. is used at each step so configuration can only make a logger /// stricter, never more verbose than the active floor. /// public LogLevel GetEffectiveMinLevel(string categoryName, LogLevel providerMinLevel) { ArgumentNullException.ThrowIfNull(categoryName); var effective = Max(Options.GetEffectiveMinLevel(), providerMinLevel); var configuredLevel = GetConfiguredMinLevel(categoryName); return configuredLevel.HasValue ? Max(effective, configuredLevel.Value) : effective; } /// /// Finds the configured category level using exact match, longest dotted-prefix match, then default fallback. /// /// The category to resolve. /// The configured level, or null when no default or override applies. private LogLevel? GetConfiguredMinLevel(string categoryName) { // Exact category configuration is the most specific and avoids unnecessary prefix scans. if (_loggerLevels.TryGetValue(categoryName, out var exactLevel)) { return exactLevel; } var bestMatchLength = -1; LogLevel? bestMatchLevel = DefaultLogLevel; foreach (var pair in _loggerLevels) { // The dotted boundary keeps sibling categories from matching by raw string prefix alone. if (!categoryName.StartsWith(pair.Key + ".", StringComparison.Ordinal)) { continue; } if (pair.Key.Length <= bestMatchLength) { continue; } bestMatchLength = pair.Key.Length; bestMatchLevel = pair.Value; } return bestMatchLevel; } /// /// Returns the stricter of two log levels. /// /// The first level. /// The second level. /// The level with the higher severity ordering. private static LogLevel Max(LogLevel left, LogLevel right) { return left > right ? left : right; } }