using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Clave.Vellum
{
///
/// Spells a term of a as one line of text and reads it back, eg:
/// Pressed(Z) & Held(Layout:ControlLeft) & ~Held(ShiftLeft).
///
///
/// The space is left off for the physical keys. Reading takes any case and spacing, a ! in place of the
/// ~, and a comma in place of the &, so a person can edit a save by hand. A term without
/// literals is the empty text and always matches.
///
public static class KeyTermText
{
public const char Negation = '~';
public const string Separator = " & ";
private static Dictionary NamesOf()
where EnumType : struct
{
var returnMe = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (EnumType value in Enum.GetValues(typeof(EnumType)))
{
string name = value.ToString();
if (returnMe.ContainsKey(name))
{
// An alias of the same value is fine, and a different value under one name is a mistake.
if (!EqualityComparer.Default.Equals(returnMe[name], value))
{
throw new InvalidOperationException(typeof(EnumType).Name + " has two values named " + name +
" apart from case.");
}
continue;
}
returnMe.Add(name, value);
}
return returnMe;
}
private static readonly Dictionary Keys = NamesOf();
private static readonly Dictionary Kinds = NamesOf();
private static readonly Dictionary Spaces = NamesOf();
private static void AppendLiteral(StringBuilder builder, KeyLiteral literal)
{
if (literal.Negated)
{
builder.Append(Negation);
}
builder.Append(literal.Kind).Append('(');
if (literal.Space != KeySpace.Physical)
{
builder.Append(literal.Space).Append(':');
}
builder.Append(literal.Key).Append(')');
}
///
/// Returns the text of the term made of .
///
public static string Format(IReadOnlyList literals)
{
if (literals == null)
{
throw new ArgumentNullException(nameof(literals));
}
var builder = new StringBuilder();
for (int i = 0; i < literals.Count; ++i)
{
if (i > 0)
{
builder.Append(Separator);
}
AppendLiteral(builder, literals[i]);
}
return builder.ToString();
}
private static InvalidDataException Bad(string text, int position, string problem)
{
return new InvalidDataException("The key term \"" + text + "\" has " + problem + " at character " +
position + ".");
}
private static int SkipSpaces(string text, int position)
{
while (position < text.Length && char.IsWhiteSpace(text[position]))
{
++position;
}
return position;
}
// The text from `position` up to the stop character, trimmed. The position is left on the stop.
private static string ReadUntil(string text, ref int position, char stop)
{
int start = position;
while (position < text.Length && text[position] != stop)
{
++position;
}
return text.Substring(start, position - start).Trim();
}
private static KeyLiteral ReadLiteral(string text, ref int position)
{
bool negated = false;
if (text[position] == Negation || text[position] == '!')
{
negated = true;
position = SkipSpaces(text, position + 1);
}
int kindStart = position;
string kindName = ReadUntil(text, ref position, '(');
KeyStateKind kind;
if (position == text.Length || !Kinds.TryGetValue(kindName, out kind))
{
throw Bad(text, kindStart, "an unknown kind of key state \"" + kindName + "\"");
}
int insideStart = ++position;
string inside = ReadUntil(text, ref position, ')');
if (position == text.Length)
{
throw Bad(text, insideStart, "no closing bracket");
}
++position;
KeySpace space = KeySpace.Physical;
int colon = inside.IndexOf(':');
if (colon >= 0)
{
string spaceName = inside.Substring(0, colon).Trim();
if (!Spaces.TryGetValue(spaceName, out space))
{
throw Bad(text, insideStart, "an unknown key space \"" + spaceName + "\"");
}
inside = inside.Substring(colon + 1).Trim();
}
KeyCode key;
if (!Keys.TryGetValue(inside, out key))
{
throw Bad(text, insideStart, "an unknown key \"" + inside + "\"");
}
return new KeyLiteral(key, kind, space, negated);
}
///
/// Reads the literals of a term back.
///
/// The text isn't a term.
public static List Parse(string text)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
var returnMe = new List();
int position = SkipSpaces(text, 0);
while (position < text.Length)
{
returnMe.Add(ReadLiteral(text, ref position));
position = SkipSpaces(text, position);
if (position == text.Length)
{
break;
}
if (text[position] != '&' && text[position] != ',')
{
throw Bad(text, position, "something other than & between its parts");
}
position = SkipSpaces(text, position + 1);
if (position == text.Length)
{
throw Bad(text, position, "nothing after the last &");
}
}
return returnMe;
}
}
}