using System;
using System.Collections.Generic;
using System.IO;
using AcDream.Core.Journal;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.UI;
///
/// Reads and writes the per-character journal file.
///
///
///
/// Retail loads on entering the world and saves whenever the notes page is
/// hidden (gmJournalUI::OnVisibilityChanged @0x004978F0) rather than
/// only at exit, so a crash costs at most the page in front of you. Both
/// moments call through here.
///
///
/// The file lives in the client's own data directory rather than beside the
/// executable, for the same reason the chat log does: the launcher replaces
/// the install atomically on update, and a file written there is wiped by the
/// next update. Register row QJ-1.
///
///
public sealed class JournalPersistence(
RuntimeJournalState journal,
string directory,
Action? report = null)
{
private readonly RuntimeJournalState _journal =
journal ?? throw new ArgumentNullException(nameof(journal));
private readonly string _directory = string.IsNullOrWhiteSpace(directory)
? throw new ArgumentException("A journal directory is required.", nameof(directory))
: directory;
private string? _characterName;
/// The file the loaded character's journal lives in, or null.
public string? CurrentPath { get; private set; }
///
/// Loads a character's journal, replacing whatever was in memory.
///
///
/// A character with no file is the ordinary first-time case and loads as an
/// empty journal. A file that fails retail's own validity check reports the
/// message and leaves the journal EMPTY rather than partially populated —
/// a half-read notebook silently loses pages, and the next save would then
/// write the loss back over the original.
///
public void Load(string characterName, string serverName = "acdream")
{
if (string.IsNullOrWhiteSpace(characterName))
return;
_characterName = characterName;
CurrentPath = Path.Combine(
_directory, JournalFile.FileNameFor(serverName, characterName));
string text;
try
{
text = File.Exists(CurrentPath) ? File.ReadAllText(CurrentPath) : string.Empty;
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{
report?.Invoke($"Problem loading journal: {e.Message}");
_journal.Load([]);
return;
}
JournalReadResult result = JournalFile.Read(text);
if (result.Error is not null)
{
report?.Invoke(result.Error);
_journal.Load([]);
return;
}
_journal.Load(result.Pages);
}
///
/// Writes the journal if anything has changed since the last write.
///
/// Whether a file was written.
public bool Save(DateTime now)
{
if (CurrentPath is null || _characterName is null)
return false;
if (!_journal.IsDirty)
return false;
IReadOnlyList pages = _journal.CaptureForSave(now);
try
{
Directory.CreateDirectory(_directory);
File.WriteAllText(CurrentPath, JournalFile.Write(pages));
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{
// Reported, not thrown: losing a note is bad, taking the client
// down while the player is writing one is worse.
report?.Invoke($"Problem saving journal: {e.Message}");
return false;
}
_journal.MarkSaved();
return true;
}
/// Saves and forgets the character — session teardown.
public void Close(DateTime now)
{
Save(now);
_characterName = null;
CurrentPath = null;
}
}