using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using PicView.Core.Config.ConfigFileManagement;
using PicView.Core.DebugTools;
using PicView.Core.FileHandling;
namespace PicView.Core.Config;
[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)]
[JsonSerializable(typeof(AppSettings))]
internal partial class SettingsGenerationContext : JsonSerializerContext;
///
/// Provides functionality to manage loading, saving, and modifying application settings.
///
public static class SettingsManager
{
///
/// Gets the file path to the currently loaded settings file, if available.
///
///
/// This property reflects the full path to the settings file currently in use by the application.
/// It is updated when settings are loaded via
/// or saved using . If no settings file is loaded,
/// the value will be null. This path can be used to reference the specific configuration file
/// being utilized or modified.
///
public static string? CurrentSettingsPath => Configuration.CorrectPath;
///
/// Gets or sets the current application settings instance, which stores all configurable
/// values for the application's behavior, appearance, and functionality.
///
///
/// This property can be used to retrieve or assign settings related to application preferences,
/// such as UI, theme, sorting, scaling, and startup configurations. The associated settings
/// are loaded via or set to defaults using
/// . Changes to the settings can be saved using
/// .
///
public static AppSettings? Settings { get; private set; }
public static SettingsConfiguration? Configuration { get; private set; }
///
/// Global Configuration Support
///
///
/// Overrides any UserSettings with GlobalSettings if they are set
///
public static GlobalSettingsConfiguration? GlobalConfig { get; private set; }
public static AppSettings? GlobalSettings { get; private set; }
///
/// Loads application settings asynchronously from a file or initializes them to default if loading fails.
///
///
/// A boolean indicating whether the settings were successfully loaded.
///
/// Thrown if deserialization of the settings file fails.
public static async ValueTask LoadSettingsAsync()
{
try
{
// Load global config (read-only, Program Path)
GlobalConfig ??= new GlobalSettingsConfiguration();
string globalPath = GlobalConfig.LocalConfigPath;
if (File.Exists(globalPath))
{
await using var globalStream = File.OpenRead(globalPath);
if (globalStream.Length > 0)
{
GlobalSettings = await JsonSerializer.DeserializeAsync(
globalStream, SettingsGenerationContext.Default.AppSettings).ConfigureAwait(false);
}
}
// Load user config (User Profile or Program Path)
Configuration ??= new SettingsConfiguration();
var userPath = ConfigFileManager.ResolveDefaultConfigPath(Configuration);
Configuration.CorrectPath = userPath;
if (File.Exists(userPath))
{
await using var userStream = File.OpenRead(userPath);
if (userStream.Length > 0)
{
Settings = await JsonSerializer.DeserializeAsync(
userStream, SettingsGenerationContext.Default.AppSettings).ConfigureAwait(false);
}
}
// Fallback to defaults if no user config found
Settings ??= GetDefaults();
// Apply Global Overrides
if (GlobalSettings != null)
ApplyOverrides(Settings, GlobalSettings);
return true;
}
catch (Exception ex)
{
DebugHelper.LogDebug(nameof(SettingsManager), nameof(LoadSettingsAsync), ex);
SetDefaults();
return false;
}
}
///
/// Asynchronously saves the current application settings to the appropriate file location.
///
///
/// Whether the settings were successfully saved.
///
public static async ValueTask SaveSettingsAsync()
{
if (Settings == null)
{
return false;
}
if (string.IsNullOrEmpty(Configuration.CorrectPath))
{
Configuration.CorrectPath = ConfigFileManager.ResolveDefaultConfigPath(Configuration);
}
if (!FileHelper.IsPathWritable(CurrentSettingsPath))
{
Configuration.CorrectPath = Configuration.RoamingConfigPath;
}
var saveLocation = await ConfigFileManager.SaveConfigFileAndReturnPathAsync(Configuration,
CurrentSettingsPath,
Settings, typeof(AppSettings), SettingsGenerationContext.Default).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(saveLocation))
{
DebugHelper.LogDebug(nameof(SettingsManager), nameof(SaveSettingsAsync), "Empty save location");
return false;
}
Configuration.CorrectPath = saveLocation;
return true;
}
private static AppSettings EnsureSettingsIfNeeded(AppSettings settings)
{
if (settings?.WindowProperties is null)
{
return GetDefaults();
}
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (settings.Version != SettingsConfiguration.CurrentSettingsVersion)
{
return EnsureSettings(settings);
}
// If navigation settings is null, it is an upgrade from an old version or the config is otherwise invalid
if (settings.Navigation is null)
{
return EnsureSettings(settings);
}
settings.Version = SettingsConfiguration.CurrentSettingsVersion;
return settings;
}
private static AppSettings EnsureSettings(AppSettings existingSettings)
{
var newSettings = GetDefaults();
existingSettings.UIProperties ??= newSettings.UIProperties;
existingSettings.Gallery ??= newSettings.Gallery;
existingSettings.Theme ??= newSettings.Theme;
existingSettings.Sorting ??= newSettings.Sorting;
existingSettings.ImageScaling ??= newSettings.ImageScaling;
existingSettings.WindowProperties ??= newSettings.WindowProperties;
existingSettings.Zoom ??= newSettings.Zoom;
existingSettings.StartUp ??= newSettings.StartUp;
existingSettings.Navigation ??= newSettings.Navigation;
existingSettings.Version = SettingsConfiguration.CurrentSettingsVersion;
return existingSettings;
}
///
/// Sets the application's settings to their default values.
///
public static void SetDefaults() => Settings = GetDefaults();
///
/// Initializes and returns an instance of the default application settings.
///
///
/// An object populated with default values.
///
public static AppSettings GetDefaults()
{
UIProperties uiProperties;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
uiProperties = new UIProperties
{
IsTaskbarProgressEnabled = false,
OpenInSameWindow = true
};
}
else
{
uiProperties = new UIProperties();
}
var settings = new AppSettings
{
UIProperties = uiProperties,
Gallery = new Gallery(),
ImageScaling = new ImageScaling(),
Sorting = new Sorting(),
Theme = new Theme(),
WindowProperties = new WindowProperties(),
Zoom = new Zoom(),
StartUp = new StartUp(),
Navigation = new Navigation(),
Version = SettingsConfiguration.CurrentSettingsVersion
};
// Get the default culture from the OS
settings.UIProperties.UserLanguage = CultureInfo.CurrentCulture.Name;
return settings;
}
///
/// Resets the application's settings to their default values and removes any existing settings file
/// to start with a clean configuration.
///
/// Thrown if an error occurs while attempting to delete the existing settings file.
/// Thrown if access to the settings file is denied during deletion.
///
/// This method ensures that the default settings are applied and deletes the user settings file if it exists,
/// allowing the application configuration to be completely reset.
///
public static void ResetDefaults()
{
try
{
DeleteFileIfExists(Configuration.TryGetCurrentUserConfigPath);
}
catch (Exception ex)
{
DebugHelper.LogDebug(nameof(SettingsManager), nameof(ResetDefaults), ex);
}
finally
{
SetDefaults();
}
return;
void DeleteFileIfExists(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
private static void ApplyOverrides(AppSettings target, AppSettings global)
{
MergeObjects(target, global);
}
///
/// Recursively merges all non-null properties from source into target.
/// Complex nested types (like UIProperties, Theme, etc.) are merged recursively.
/// Value types and simple properties are directly overwritten.
///
private static void MergeObjects(object? target, object? source)
{
if (target == null || source == null)
return;
var targetType = target.GetType();
var sourceType = source.GetType();
foreach (var prop in sourceType.GetProperties())
{
var sourceValue = prop.GetValue(source);
if (sourceValue == null)
continue;
var targetProp = targetType.GetProperty(prop.Name);
if (targetProp == null || !targetProp.CanWrite)
continue;
var targetValue = targetProp.GetValue(target);
// If this is a nested object (class) and not a string, merge recursively
if (prop.PropertyType.IsClass && prop.PropertyType != typeof(string))
{
if (targetValue == null)
{
// If user doesn't have that object at all, copy it fully
targetProp.SetValue(target, sourceValue);
}
else
{
// Recursively merge individual properties
MergeObjects(targetValue, sourceValue);
}
}
else
{
// Simple value type or string – overwrite directly
targetProp.SetValue(target, sourceValue);
}
}
}
}