using System.Text;
namespace AcDream.App.UI.Layout;
///
/// Exact port of retail's string-table escape codec
/// (StringTableMetaLanguage::UnescapeString @ 0x0067BDC0 /
/// EscapeString @ 0x0067BBC0 and their character tables
/// GetUnEscapedChar @ 0x0067B750 / GetEscapedChar @ 0x0067B6C0).
///
///
///
/// PLACEMENT (the systemic 2026-08-17 normalization round): retail decodes
/// escapes at the string SOURCE, not per-widget. Every public
/// StringInfo resolution runs the unescape unconditionally before any
/// consumer sees the text — StringInfo::InqString @ 0x0042E490 tail
/// and StringInfo::GetLiteralValue @ 0x0042CA50 both end in
/// UnescapeString. The write side is the inverse:
/// StringInfo::SetLiteralValue @ 0x0042C980 runs EscapeString
/// when storing plain text (and StringInfo::AddVariable_String
/// @ 0x0042E6C0 always stores variables that way), so stored text is
/// escaped, resolved text is decoded, and variable content round-trips
/// verbatim. acdream's equivalent source is ;
/// widgets and controllers receive already-decoded strings and must not
/// re-decode (a second pass corrupts an authored \\n — escaped
/// backslash then 'n' — into a line break).
///
///
/// The escape set (byte-verified against the PDB-paired 2013 binary; the
/// metalanguage character-set literal at file offset 0x3FE178 is the ten
/// characters []!{}#\|^$):
/// \n → LF (0x0A), \t → TAB (0x09), \r → CR (0x0D),
/// \q → '"' (0x22), and a backslash before any of the ten
/// metalanguage characters yields that character itself. A backslash before
/// anything else is NOT an escape — retail copies it through verbatim
/// (GetUnEscapedChar returns 0 and UnescapeString's
/// else-branch keeps the current character).
///
///
public static class RetailStringEscapes
{
/// The ten metalanguage-significant characters that escape to
/// themselves. Byte-decoded from the retail binary (see class remarks) —
/// the same literal both character tables test with wcschr.
private const string MetaCharacters = "[]!{}#\\|^$";
///
/// StringTableMetaLanguage::GetUnEscapedChar @ 0x0067B750: the
/// character an escape pair \+ decodes
/// to, or '\0' when the pair is not an escape.
///
internal static char GetUnEscapedChar(char value) => value switch
{
'n' => '\n',
'q' => '"',
'r' => '\r',
't' => '\t',
not '\0' when MetaCharacters.Contains(value) => value,
_ => '\0',
};
///
/// StringTableMetaLanguage::GetEscapedChar @ 0x0067B6C0: the
/// character that follows the backslash when
/// must be stored escaped, or '\0' when it is stored verbatim.
///
internal static char GetEscapedChar(char value) => value switch
{
'\t' => 't',
'\n' => 'n',
'\r' => 'r',
'"' => 'q',
not '\0' when MetaCharacters.Contains(value) => value,
_ => '\0',
};
///
/// StringTableMetaLanguage::UnescapeString @ 0x0067BDC0: decodes
/// every two-character escape pair; all other characters (including a
/// backslash that does not start a recognized pair, and a trailing
/// backslash) copy through verbatim.
///
public static string Unescape(string value)
{
ArgumentNullException.ThrowIfNull(value);
// Fast path: a string with no backslash cannot contain an escape.
int first = value.IndexOf('\\');
if (first < 0)
return value;
var result = new StringBuilder(value.Length);
for (int i = 0; i < value.Length; i++)
{
char current = value[i];
// Retail reads the character AFTER the candidate backslash (the
// terminator — never an escape — when at the end of the buffer).
char next = i + 1 < value.Length ? value[i + 1] : '\0';
char unescaped = GetUnEscapedChar(next);
if (current == '\\' && unescaped != '\0')
{
result.Append(unescaped);
i++; // consume the pair
}
else if (current != '\0')
{
result.Append(current);
}
}
return result.ToString();
}
///
/// StringTableMetaLanguage::EscapeString @ 0x0067BBC0: the exact
/// inverse — every character with a mapping
/// is stored as \ + that mapping; everything else verbatim.
/// Unescape(Escape(x)) == x for every —
/// the round-trip retail relies on for template variables.
///
public static string Escape(string value)
{
ArgumentNullException.ThrowIfNull(value);
StringBuilder? result = null;
for (int i = 0; i < value.Length; i++)
{
char current = value[i];
char escaped = GetEscapedChar(current);
if (escaped != '\0')
{
result ??= new StringBuilder(value.Length + 4)
.Append(value, 0, i);
result.Append('\\').Append(escaped);
}
else if (current != '\0')
{
result?.Append(current);
}
}
return result?.ToString() ?? value;
}
}