namespace ParadoxSaveParser.Lib;
///
/// The node a search expression is tested against: its key inside the parent dictionary,
/// still in the raw bytes of the save, and its position inside the parent list.
/// Keys stay undecoded so that a key rejected by the query never becomes a string.
///
public readonly ref struct MatchCandidate
{
public readonly ReadOnlySpan Key;
public readonly int Index;
/// Encoding is written in.
public readonly Encoding Encoding;
/// A list item, which has a position but no key.
public MatchCandidate(int index)
{
Key = default;
Index = index;
Encoding = Encoding.Latin1;
}
public MatchCandidate(int index, ReadOnlySpan key, Encoding encoding)
{
Key = key;
Index = index;
Encoding = encoding;
}
}
public interface ISearchExpression
{
bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression);
}
public static class SearchExpressionCompiler
{
private static bool CharEqualsAndNotEscaped(char c, ReadOnlySpan chars, int i)
=> chars[i] == c && (i < 1 || chars[i - 1] != '\\') && (i < 2 || chars[i - 2] != '\\');
public static ISearchExpression Compile(ReadOnlySpan query)
{
if (query.IsEmpty)
throw new ArgumentNullException(nameof(query));
if (query[0] is '(')
{
var subExprs = new List();
int supExprBegin = 1;
int bracketBalance = 1;
int i = supExprBegin;
for (; i < query.Length && bracketBalance != 0; i++)
{
if (CharEqualsAndNotEscaped('(', query, i))
{
bracketBalance++;
}
else if (CharEqualsAndNotEscaped(')', query, i))
{
bracketBalance--;
}
else if (bracketBalance == 1 && CharEqualsAndNotEscaped('|', query, i))
{
var subPart = query.Slice(supExprBegin, i - supExprBegin);
var subExpr = Compile(subPart);
subExprs.Add(subExpr);
supExprBegin = i + 1;
}
}
if (i != query.Length)
throw new NotImplementedException("Expressions after ')' are not supported");
if (bracketBalance > 0)
throw new Exception("Too many opening brackets");
if (bracketBalance < 0)
throw new Exception("Too many closing brackets");
var subPartLast = query.Slice(supExprBegin, i - 1 - supExprBegin);
var subExprLast = Compile(subPartLast);
subExprs.Add(subExprLast);
return new MultipleMatchExpression(subExprs);
}
int partBeforePointLength = 0;
while (partBeforePointLength < query.Length)
{
if (CharEqualsAndNotEscaped('.', query, partBeforePointLength))
break;
partBeforePointLength++;
}
var part = query.Slice(0, partBeforePointLength);
ReadOnlySpan remaining = default;
if (partBeforePointLength < query.Length)
remaining = query.Slice(partBeforePointLength + 1);
if (part is "*")
return new AnyMatchExpression(remaining.IsEmpty ? null : Compile(remaining));
if (part is "~")
return new NoMatchExpression();
for (int j = 0; j < part.Length; j++)
if (CharEqualsAndNotEscaped('*', part, j))
throw new NotImplementedException("pattern matching other than '*' is not implemented yet");
if (part[0] is '[')
{
part = part.Slice(1, part.Length - 2);
return new IndexMatchExpression(int.Parse(part), remaining.IsEmpty ? null : Compile(remaining));
}
return new ExactMatchExpression(part.ToString(), remaining.IsEmpty ? null : Compile(remaining));
}
private record AnyMatchExpression(ISearchExpression? next) : ISearchExpression
{
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
{
nextSearchExpression = next;
return true;
}
}
private record NoMatchExpression : ISearchExpression
{
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
{
nextSearchExpression = null;
return false;
}
}
private record MultipleMatchExpression(List subExprs) : ISearchExpression
{
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
{
foreach (var e in subExprs)
if (e.DoesMatch(candidate, out nextSearchExpression))
return true;
nextSearchExpression = null;
return false;
}
}
private record IndexMatchExpression(int index, ISearchExpression? next) : ISearchExpression
{
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
{
if (candidate.Index == index)
{
nextSearchExpression = next;
return true;
}
nextSearchExpression = null;
return false;
}
}
private record ExactMatchExpression(string key, ISearchExpression? next) : ISearchExpression
{
// the key is compared as bytes, so it is encoded once for whatever encoding the
// parser reads the save in
private byte[]? _keyBytes;
private Encoding? _keyEncoding;
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
{
if (!ReferenceEquals(_keyEncoding, candidate.Encoding))
{
_keyEncoding = candidate.Encoding;
_keyBytes = candidate.Encoding.GetBytes(key);
}
if (candidate.Key.SequenceEqual(_keyBytes))
{
nextSearchExpression = next;
return true;
}
nextSearchExpression = null;
return false;
}
}
}