BareGit
#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"));
}