#include "game_field.h"
#include <algorithm>
#include <charconv>
#include <cstdint>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
bool validGameText(std::string_view value)
{
const auto* current = reinterpret_cast<const unsigned char*>(
value.data());
const auto* end = current + value.size();
while(current != end)
{
const unsigned char first = *current++;
if(first <= 0x7f)
{
if(first == 0)
{
return false;
}
continue;
}
std::size_t remaining = 0;
std::uint32_t code_point = 0;
std::uint32_t minimum = 0;
if(first >= 0xc2 && first <= 0xdf)
{
remaining = 1;
code_point = first & 0x1f;
minimum = 0x80;
}
else if(first >= 0xe0 && first <= 0xef)
{
remaining = 2;
code_point = first & 0x0f;
minimum = 0x800;
}
else if(first >= 0xf0 && first <= 0xf4)
{
remaining = 3;
code_point = first & 0x07;
minimum = 0x10000;
}
else
{
return false;
}
if(static_cast<std::size_t>(end - current) < remaining)
{
return false;
}
for(std::size_t index = 0; index < remaining; ++index)
{
const unsigned char continuation = *current++;
if((continuation & 0xc0) != 0x80)
{
return false;
}
code_point = (code_point << 6) | (continuation & 0x3f);
}
if(code_point < minimum || code_point > 0x10ffff ||
(code_point >= 0xd800 && code_point <= 0xdfff))
{
return false;
}
}
return true;
}
namespace
{
mw::Error fieldError(const GameField& field, std::string message)
{
return GameFieldValidationError{
field.key,
"Invalid field '" + field.key + "': " + std::move(message)};
}
mw::E<std::int64_t> parseInteger(
const GameField& field, const std::string& value)
{
if(value.empty() || value.front() == '+' || value.front() == ' ' ||
value.back() == ' ')
{
return std::unexpected(fieldError(field, "expected an integer"));
}
std::int64_t result = 0;
const auto parsed = std::from_chars(
value.data(), value.data() + value.size(), result, 10);
if(parsed.ec != std::errc() ||
parsed.ptr != value.data() + value.size())
{
return std::unexpected(fieldError(field, "expected an integer"));
}
return result;
}
} // namespace
mw::E<std::vector<GameFieldValue>> decodeGameFields(
const GameDefinitionSnapshot& definition,
const SubmittedGameFields& submitted)
{
std::unordered_set<std::string_view> known_keys;
known_keys.reserve(definition.fields.size());
for(const GameField& field : definition.fields)
{
known_keys.insert(field.key);
}
for(const auto& [key, value] : submitted)
{
[[maybe_unused]] const std::string& ignored_value = value;
if(!known_keys.contains(key))
{
return std::unexpected(mw::Error(GameFieldValidationError{
key, "Unknown custom field '" + key + "'"}));
}
}
std::vector<GameFieldValue> result;
result.reserve(submitted.size());
for(const GameField& field : definition.fields)
{
const auto position = submitted.find(field.key);
if(position == submitted.end() || position->second.empty())
{
continue;
}
const std::string& value = position->second;
if(!validGameText(value))
{
return std::unexpected(fieldError(field, "expected UTF-8 text"));
}
if(field.type == GameFieldType::INTEGER)
{
auto parsed = parseInteger(field, value);
if(!parsed)
{
return std::unexpected(std::move(parsed.error()));
}
result.push_back({field.id, field.type, *parsed});
continue;
}
if(field.type == GameFieldType::CHOICE)
{
const bool allowed = std::ranges::any_of(
field.choices,
[&value](const GameChoice& choice)
{
return choice.value == value;
});
if(!allowed)
{
return std::unexpected(
fieldError(field, "unknown choice"));
}
}
result.push_back({field.id, field.type, value});
}
return result;
}
std::string_view gameFieldTypeName(GameFieldType type)
{
switch(type)
{
case GameFieldType::INTEGER:
return "INTEGER";
case GameFieldType::STRING:
return "STRING";
case GameFieldType::CHOICE:
return "CHOICE";
}
return "UNKNOWN";
}
mw::E<GameFieldType> parseGameFieldType(std::string_view value)
{
if(value == "INTEGER")
{
return GameFieldType::INTEGER;
}
if(value == "STRING")
{
return GameFieldType::STRING;
}
if(value == "CHOICE")
{
return GameFieldType::CHOICE;
}
return std::unexpected(mw::runtimeError("Unknown game field type"));
}