BareGit
#include "public_id.h"

#include <algorithm>
#include <array>
#include <charconv>
#include <cctype>
#include <cstdint>
#include <limits>
#include <string>
#include <system_error>

namespace
{

mw::E<std::string> formatNumber(std::uint64_t number, int base)
{
    std::array<char, std::numeric_limits<std::uint64_t>::digits + 1> buffer;
    const auto result = std::to_chars(
        buffer.data(),
        buffer.data() + buffer.size(),
        number,
        base);
    if(result.ec != std::errc{})
    {
        return std::unexpected(
            mw::runtimeError("Failed to format a public card number"));
    }
    return std::string(buffer.data(), result.ptr);
}

bool isDigit(char character)
{
    return std::isdigit(static_cast<unsigned char>(character)) != 0;
}

bool isLowercaseAlphaNumeric(std::string_view value)
{
    return !value.empty() && std::ranges::all_of(
        value,
        [](char character)
        {
            return (character >= 'a' && character <= 'z') ||
                   (character >= '0' && character <= '9');
        });
}

template<typename Number>
mw::E<Number> parseNumber(std::string_view value, int base)
{
    Number number = 0;
    const auto result = std::from_chars(
        value.data(), value.data() + value.size(), number, base);
    if(value.empty() || result.ec != std::errc{} ||
       result.ptr != value.data() + value.size())
    {
        return std::unexpected(mw::runtimeError("Invalid public card ID"));
    }
    return number;
}

std::size_t runEnd(std::string_view value, std::size_t begin, bool digit)
{
    std::size_t end = begin;
    while(end < value.size() && isDigit(value[end]) == digit)
    {
        ++end;
    }
    return end;
}

int compareNumericRuns(std::string_view left, std::string_view right)
{
    const std::size_t left_nonzero = left.find_first_not_of('0');
    const std::size_t right_nonzero = right.find_first_not_of('0');
    const std::string_view left_value = left_nonzero == std::string_view::npos
        ? left.substr(left.size() - 1)
        : left.substr(left_nonzero);
    const std::string_view right_value = right_nonzero == std::string_view::npos
        ? right.substr(right.size() - 1)
        : right.substr(right_nonzero);

    if(left_value.size() != right_value.size())
    {
        return left_value.size() < right_value.size() ? -1 : 1;
    }
    const int value_comparison = left_value.compare(right_value);
    if(value_comparison != 0)
    {
        return value_comparison;
    }
    if(left.size() != right.size())
    {
        return left.size() < right.size() ? -1 : 1;
    }
    return 0;
}

} // namespace

mw::E<std::string> formatPublicId(const CardIdentity& identity)
{
    if(identity.game_short_name)
    {
        if(identity.game_short_name->empty() || identity.card_number == 0 ||
           identity.card_number >
               static_cast<std::uint64_t>(
                   std::numeric_limits<std::int64_t>::max()))
        {
            return std::unexpected(
                mw::runtimeError("Invalid game card identity"));
        }
        auto number = formatNumber(identity.card_number, 10);
        if(!number)
        {
            return std::unexpected(std::move(number.error()));
        }
        return *identity.game_short_name + "-" + *number;
    }

    if(identity.card_number > std::numeric_limits<std::uint32_t>::max())
    {
        return std::unexpected(mw::runtimeError("Invalid loose card identity"));
    }
    return formatNumber(identity.card_number, 36);
}

mw::E<CardIdentity> parsePublicId(std::string_view public_id)
{
    const std::size_t separator = public_id.find('-');
    CardIdentity identity;
    if(separator == std::string_view::npos)
    {
        if(!isLowercaseAlphaNumeric(public_id))
        {
            return std::unexpected(
                mw::runtimeError("Invalid public card ID"));
        }
        auto number = parseNumber<std::uint32_t>(public_id, 36);
        if(!number)
        {
            return std::unexpected(std::move(number.error()));
        }
        identity = {std::nullopt, *number};
    }
    else
    {
        const std::string_view game = public_id.substr(0, separator);
        const std::string_view number_text = public_id.substr(separator + 1);
        if(!isLowercaseAlphaNumeric(game) ||
           number_text.empty() ||
           number_text.front() == '0' ||
           !std::ranges::all_of(number_text, isDigit) ||
           number_text.find('-') != std::string_view::npos)
        {
            return std::unexpected(
                mw::runtimeError("Invalid public card ID"));
        }
        auto number = parseNumber<std::int64_t>(number_text, 10);
        if(!number || *number <= 0)
        {
            return std::unexpected(
                mw::runtimeError("Invalid public card ID"));
        }
        identity = {
            std::string(game),
            static_cast<std::uint64_t>(*number),
        };
    }

    auto canonical_id = formatPublicId(identity);
    if(!canonical_id || *canonical_id != public_id)
    {
        return std::unexpected(mw::runtimeError("Invalid public card ID"));
    }
    return identity;
}

bool naturalPublicIdLess(std::string_view left, std::string_view right)
{
    std::size_t left_index = 0;
    std::size_t right_index = 0;
    while(left_index < left.size() && right_index < right.size())
    {
        const bool left_digit = isDigit(left[left_index]);
        const bool right_digit = isDigit(right[right_index]);
        const std::size_t left_end = runEnd(left, left_index, left_digit);
        const std::size_t right_end = runEnd(right, right_index, right_digit);
        const std::string_view left_run =
            left.substr(left_index, left_end - left_index);
        const std::string_view right_run =
            right.substr(right_index, right_end - right_index);

        int comparison;
        if(left_digit && right_digit)
        {
            comparison = compareNumericRuns(left_run, right_run);
        }
        else
        {
            comparison = left_run.compare(right_run);
        }
        if(comparison != 0)
        {
            return comparison < 0;
        }

        left_index = left_end;
        right_index = right_end;
    }
    return left.size() < right.size();
}