BareGit
#include "window_adapter.hpp"

#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <exception>
#include <stdexcept>
#include <utility>

extern "C"
{
#include "hack.h"
#include "func_tab.h"
}

namespace nethack_mcp
{

namespace
{

using ShimCallback = void (*)(const char*, void*, const char*, ...);

extern "C" void shim_graphics_set_callback(ShimCallback callback);

constexpr int MAP_WIDTH = 79;
constexpr int MAP_HEIGHT = 21;
constexpr std::size_t MAX_MESSAGES = 10;
constexpr std::size_t MAX_TEXT_BYTES = 4096;
constexpr const char* STATUS_NAMES[MAXBLSTATS] = {
    "title", "strength", "dexterity", "constitution", "intelligence",
    "wisdom", "charisma", "alignment", "score", "carrying-capacity",
    "gold", "power", "power-max", "experience-level", "armor-class",
    "HD", "time", "hunger", "hitpoints", "hitpoints-max",
    "dungeon-level", "experience", "condition", "weapon", "armor",
    "terrain", "version",
};

std::string copyString(const char* value)
{
    return value == nullptr ? std::string() : std::string(value);
}

std::string statusKey(const std::string& name)
{
    if(name == "hitpoints") return "hp";
    if(name == "hitpoints-max") return "max_hp";
    if(name == "power") return "power";
    if(name == "power-max") return "max_power";
    std::string key = name;
    std::replace(key.begin(), key.end(), '-', '_');
    return key;
}

Json statusText(const char* value, bool parse_number)
{
    const std::string encoded = copyString(value);
    std::string display(encoded.size() + 1, '\0');
    decode_mixed(display.data(), encoded.c_str());
    display.resize(std::strlen(display.c_str()));
    Json result = {{"text", display}};
    long long number = 0;
    const auto parsed = std::from_chars(display.data(),
                                        display.data() + display.size(),
                                        number);
    if(parse_number && !display.empty() && parsed.ec == std::errc()
       && parsed.ptr == display.data() + display.size())
    {
        result["value"] = number;
    }
    return result;
}

} // namespace

WindowAdapter* WindowAdapter::active_adapter_ = nullptr;

WindowAdapter::WindowAdapter(FramedChannel& channel, std::string game_id)
        : channel_(channel), game_id_(std::move(game_id)),
          map_rows_(MAP_HEIGHT, std::string(MAP_WIDTH, ' '))
{}

void WindowAdapter::install()
{
    active_adapter_ = this;
    shim_graphics_set_callback(&WindowAdapter::callback);
}

void WindowAdapter::callback(const char* name, void* return_ptr,
                            const char* format, ...)
{
    if(active_adapter_ == nullptr)
    {
        return;
    }

    va_list arguments;
    va_start(arguments, format);
    try
    {
        active_adapter_->handleCallback(name, return_ptr, format, arguments);
    }
    catch(const std::exception& exception)
    {
        std::fprintf(stderr, "NetHack callback failed: %s\n",
                     exception.what());
        std::_Exit(2);
    }
    catch(...)
    {
        std::fputs("NetHack callback failed with an unknown exception\n",
                   stderr);
        std::_Exit(2);
    }
    va_end(arguments);
}

void WindowAdapter::handleCallback(const char* name, void* return_ptr,
                                   const char* format,
                                   std::va_list arguments)
{
    if(name == nullptr)
    {
        throw std::runtime_error("NetHack sent a nameless callback");
    }

    if(std::strcmp(name, "shim_init_nhwindows") == 0)
    {
        return;
    }

    if(std::strcmp(name, "shim_create_nhwindow") == 0)
    {
        const int type = va_arg(arguments, int);
        const int window = next_window_id_++;
        window_types_[window] = type;
        setIntegerReturn(return_ptr, window);
        return;
    }

    if(std::strcmp(name, "shim_destroy_nhwindow") == 0)
    {
        return;
    }

    if(std::strcmp(name, "shim_clear_nhwindow") == 0)
    {
        const int window = va_arg(arguments, int);
        if(window_types_[window] == NHW_MAP)
        {
            map_rows_.assign(MAP_HEIGHT, std::string(MAP_WIDTH, ' '));
        }
        return;
    }

    if(std::strcmp(name, "shim_curs") == 0)
    {
        const int window = va_arg(arguments, int);
        const int x = va_arg(arguments, int);
        const int y = va_arg(arguments, int);
        if(window_types_[window] == NHW_MAP)
        {
            cursor_x_ = x;
            cursor_y_ = y;
        }
        return;
    }

    if(std::strcmp(name, "shim_print_glyph") == 0)
    {
        const int window = va_arg(arguments, int);
        const int x = va_arg(arguments, int);
        const int y = va_arg(arguments, int);
        const auto* glyph = va_arg(arguments, const glyph_info*);
        (void) va_arg(arguments, const glyph_info*);
        if(window_types_[window] == NHW_MAP && glyph != nullptr
           && y >= 0 && y < MAP_HEIGHT
           && x >= 1 && x <= MAP_WIDTH)
        {
            const unsigned char symbol = static_cast<unsigned char>(
                glyph->ttychar);
            map_rows_[y][x - 1] = std::isprint(symbol)
                ? static_cast<char>(symbol) : '?';
        }
        return;
    }

    if(std::strcmp(name, "shim_putstr") == 0)
    {
        const int window = va_arg(arguments, int);
        (void) va_arg(arguments, int);
        const char* text = va_arg(arguments, const char*);
        if(text == nullptr)
        {
            return;
        }
        if(window_types_[window] == NHW_STATUS)
        {
            status_["text"] = text;
        }
        else if(window_types_[window] == NHW_MESSAGE)
        {
            addMessage(text);
        }
        else if(window_types_[window] == NHW_TEXT)
        {
            text_window_.append(text);
            if(text_window_.size() > MAX_TEXT_BYTES)
            {
                text_window_.resize(MAX_TEXT_BYTES);
            }
        }
        return;
    }

    if(std::strcmp(name, "shim_raw_print") == 0
       || std::strcmp(name, "shim_raw_print_bold") == 0)
    {
        addMessage(va_arg(arguments, const char*));
        return;
    }

    if(std::strcmp(name, "shim_putmsghistory") == 0)
    {
        addMessage(va_arg(arguments, const char*));
        (void) va_arg(arguments, int);
        return;
    }

    if(std::strcmp(name, "shim_start_menu") == 0)
    {
        active_menu_window_ = va_arg(arguments, int);
        (void) va_arg(arguments, unsigned long);
        menu_entries_.clear();
        menu_prompt_.clear();
        next_menu_entry_id_ = 1;
        return;
    }

    if(std::strcmp(name, "shim_add_menu") == 0)
    {
        (void) va_arg(arguments, int);
        (void) va_arg(arguments, const glyph_info*);
        const auto* identifier = va_arg(arguments, const anything*);
        const int letter = va_arg(arguments, int);
        (void) va_arg(arguments, int);
        (void) va_arg(arguments, int);
        (void) va_arg(arguments, int);
        const char* text = va_arg(arguments, const char*);
        const unsigned item_flags = va_arg(arguments, unsigned int);
        MenuEntry entry;
        entry.entry_id = next_menu_entry_id_++;
        entry.text = copyString(text);
        entry.selectable = identifier != nullptr;
        entry.selected = (item_flags & MENU_ITEMFLAGS_SELECTED) != 0;
        entry.letter = static_cast<char>(letter);
        if(identifier != nullptr)
        {
            entry.identifier.resize(sizeof(anything));
            std::memcpy(entry.identifier.data(), identifier,
                        sizeof(anything));
        }
        menu_entries_.push_back(std::move(entry));
        return;
    }

    if(std::strcmp(name, "shim_end_menu") == 0)
    {
        (void) va_arg(arguments, int);
        menu_prompt_ = copyString(va_arg(arguments, const char*));
        return;
    }

    if(std::strcmp(name, "shim_select_menu") == 0)
    {
        const int window = va_arg(arguments, int);
        const int how = va_arg(arguments, int);
        auto** menu_list = va_arg(arguments, menu_item**);
        if(inventory_requested_ && window == active_menu_window_
           && how == PICK_ONE && menu_prompt_.empty())
        {
            Json entries = Json::array();
            for(const MenuEntry& entry : menu_entries_)
            {
                const unsigned char letter =
                    static_cast<unsigned char>(entry.letter);
                if(entry.selectable && std::isprint(letter))
                {
                    entries.push_back({
                        {"letter", std::string(1, entry.letter)},
                        {"text", entry.text},
                    });
                }
            }
            inventory_["known"] = true;
            inventory_["stale"] = false;
            inventory_["entries"] = std::move(entries);
        }
        inventory_requested_ = false;
        Json pending = makePending("menu", name);
        pending["mode"] = how;
        pending["prompt"] = menu_prompt_;
        pending["entries"] = Json::array();
        for(const MenuEntry& entry : menu_entries_)
        {
            pending["entries"].push_back({
                {"entry_id", entry.entry_id},
                {"text", entry.text},
                {"selectable", entry.selectable},
                {"selected", entry.selected},
            });
        }
        const Json response = waitForInput(std::move(pending));
        if(menu_list == nullptr || response.value("cancel", false))
        {
            setIntegerReturn(return_ptr, 0);
            return;
        }

        const Json selections = response.value("selections", Json::array());
        if(selections.empty())
        {
            setIntegerReturn(return_ptr, 0);
            return;
        }
        auto* selected = static_cast<menu_item*>(std::malloc(
            sizeof(menu_item) * selections.size()));
        if(selected == nullptr)
        {
            throw std::runtime_error("could not allocate menu response");
        }
        std::size_t selected_count = 0;
        for(const Json& item : selections)
        {
            const int entry_id = item.at("entry_id").get<int>();
            const auto found = std::find_if(
                menu_entries_.begin(), menu_entries_.end(),
                [entry_id](const MenuEntry& entry) {
                    return entry.entry_id == entry_id && entry.selectable;
                });
            if(found == menu_entries_.end())
            {
                std::free(selected);
                throw std::runtime_error("unknown menu entry response");
            }
            std::memset(&selected[selected_count].item, 0,
                        sizeof(selected[selected_count].item));
            std::memcpy(&selected[selected_count].item,
                        found->identifier.data(), sizeof(anything));
            selected[selected_count].count = item.value("count", -1L);
            selected[selected_count].itemflags =
                MENU_ITEMFLAGS_SELECTED;
            ++selected_count;
        }
        *menu_list = selected;
        setIntegerReturn(return_ptr, static_cast<int>(selected_count));
        return;
    }

    if(std::strcmp(name, "shim_message_menu") == 0)
    {
        const int letter = va_arg(arguments, int);
        const int how = va_arg(arguments, int);
        const std::string label = copyString(va_arg(arguments,
                                                    const char*));
        if(inventory_requested_ && how == PICK_ONE
           && std::isprint(static_cast<unsigned char>(letter)))
        {
            inventory_["known"] = true;
            inventory_["stale"] = false;
            inventory_["entries"] = Json::array({{
                {"letter", std::string(1, static_cast<char>(letter))},
                {"text", label},
            }});
        }
        inventory_requested_ = false;
        addMessage(label.c_str());
        setCharacterReturn(return_ptr, '\0');
        return;
    }

    if(std::strcmp(name, "shim_display_file") == 0)
    {
        const std::string filename = copyString(
            va_arg(arguments, const char*));
        (void) va_arg(arguments, int);
        Json pending = makePending("acknowledge", name);
        pending["filename"] = filename;
        pending["text"] = text_window_;
        (void) waitForInput(std::move(pending));
        text_window_.clear();
        return;
    }

    if(std::strcmp(name, "shim_display_nhwindow") == 0)
    {
        const int window = va_arg(arguments, int);
        const int blocking = va_arg(arguments, int);
        if(blocking != 0 && window_types_[window] != NHW_MENU)
        {
            Json pending = makePending("acknowledge", name);
            pending["text"] = text_window_;
            (void) waitForInput(std::move(pending));
            text_window_.clear();
        }
        return;
    }

    if(std::strcmp(name, "shim_wait_synch") == 0)
    {
        Json pending = makePending("acknowledge", name);
        (void) waitForInput(std::move(pending));
        return;
    }

    if(std::strcmp(name, "shim_nhgetch") == 0
       || std::strcmp(name, "shim_nh_poskey") == 0)
    {
        Json pending = makePending("key", name);
        const Json response = waitForInput(std::move(pending));
        const int value = response.value("value", 27);
        inventory_requested_ = value == 'i';
        if(std::strcmp(name, "shim_nh_poskey") == 0)
        {
            auto* x = va_arg(arguments, coordxy*);
            auto* y = va_arg(arguments, coordxy*);
            auto* modifier = va_arg(arguments, int*);
            if(x != nullptr) *x = static_cast<coordxy>(cursor_x_);
            if(y != nullptr) *y = static_cast<coordxy>(cursor_y_);
            if(modifier != nullptr) *modifier = 0;
        }
        setIntegerReturn(return_ptr, value);
        return;
    }

    if(std::strcmp(name, "shim_yn_function") == 0)
    {
        const std::string query = copyString(va_arg(arguments, const char*));
        const char* choice_list = va_arg(arguments, const char*);
        const std::string choices = copyString(choice_list);
        const int default_value = va_arg(arguments, int);
        Json pending = makePending("choice", name);
        pending["query"] = query;
        pending["choices"] = choices;
        pending["has_choices"] = choice_list != nullptr;
        pending["default"] = default_value;
        const Json response = waitForInput(std::move(pending));
        const int value = response.value("value", 27);
        setCharacterReturn(return_ptr, static_cast<char>(value));
        return;
    }

    if(std::strcmp(name, "shim_getlin") == 0)
    {
        const std::string query = copyString(va_arg(arguments, const char*));
        auto* buffer = va_arg(arguments, char*);
        Json pending = makePending("text", name);
        pending["query"] = query;
        pending["max_bytes"] = 255;
        const Json response = waitForInput(std::move(pending));
        const std::string text = response.value("cancel", false)
            ? std::string(1, 27)
            : response.value("text", std::string());
        if(buffer != nullptr)
        {
            const std::size_t length = std::min<std::size_t>(
                text.size(), 255);
            std::memcpy(buffer, text.data(), length);
            buffer[length] = '\0';
        }
        return;
    }

    if(std::strcmp(name, "shim_get_ext_cmd") == 0)
    {
        Json pending = makePending("command", name);
        pending["commands"] = Json::array();
        for(const ext_func_tab* command = extcmdlist;
            command != nullptr && command->ef_txt != nullptr; ++command)
        {
            pending["commands"].push_back(command->ef_txt);
        }
        const Json response = waitForInput(std::move(pending));
        setIntegerReturn(return_ptr,
                         resolveCommand(response.value("command", "")));
        return;
    }

    if(std::strcmp(name, "shim_get_color_string") == 0)
    {
        copyTextReturn(return_ptr, "");
        return;
    }

    if(std::strcmp(name, "shim_getmsghistory") == 0)
    {
        (void) va_arg(arguments, int);
        if(return_ptr != nullptr)
        {
            *static_cast<char**>(return_ptr) = nullptr;
        }
        return;
    }

    if(std::strcmp(name, "shim_update_inventory") == 0)
    {
        inventory_["stale"] = true;
        return;
    }

    if(std::strcmp(name, "shim_status_init") == 0)
    {
        status_.clear();
        status_fields_.clear();
        for(int field = 0; field < MAXBLSTATS; ++field)
        {
            status_fields_[field] = statusKey(STATUS_NAMES[field]);
        }
        return;
    }

    if(std::strcmp(name, "shim_status_enablefield") == 0)
    {
        const int field = va_arg(arguments, int);
        const std::string name = copyString(va_arg(arguments, const char*));
        (void) va_arg(arguments, const char*);
        const int enabled = va_arg(arguments, int);
        if(field >= 0 && field < MAXBLSTATS)
        {
            const std::string key = statusKey(name);
            if(enabled)
            {
                status_fields_[field] = key;
            }
            else
            {
                status_.erase(key);
                status_fields_.erase(field);
            }
        }
        return;
    }

    if(std::strcmp(name, "shim_status_update") == 0)
    {
        const int field = va_arg(arguments, int);
        const void* value = va_arg(arguments, const void*);
        (void) va_arg(arguments, int);
        (void) va_arg(arguments, int);
        (void) va_arg(arguments, int);
        (void) va_arg(arguments, const unsigned long*);
        const auto found = status_fields_.find(field);
        if(found == status_fields_.end() || value == nullptr)
        {
            return;
        }
        if(field == BL_CONDITION)
        {
            const unsigned long mask = *static_cast<const unsigned long*>(
                value);
            Json active = Json::array();
            for(int index = 0; index < CONDITION_COUNT; ++index)
            {
                if(mask & conditions[index].mask)
                {
                    active.push_back(conditions[index].text[0]);
                }
            }
            status_[found->second] = {
                {"mask", mask}, {"active", std::move(active)},
            };
        }
        else
        {
            Json formatted = statusText(
                static_cast<const char*>(value), field != BL_HUNGER);
            if(field == BL_GOLD)
            {
                const std::string display =
                    formatted.at("text").get<std::string>();
                const std::size_t colon = display.rfind(':');
                if(colon != std::string::npos)
                {
                    long long amount = 0;
                    const char* first = display.data() + colon + 1;
                    const char* last = display.data() + display.size();
                    const auto parsed = std::from_chars(first, last, amount);
                    if(first != last && parsed.ec == std::errc()
                       && parsed.ptr == last)
                    {
                        formatted["text"] = std::to_string(amount);
                        formatted["value"] = amount;
                    }
                }
            }
            status_[found->second] = std::move(formatted);
        }
        return;
    }

    if(std::strcmp(name, "shim_exit_nhwindows") == 0)
    {
        addMessage(va_arg(arguments, const char*));
        return;
    }

    if(std::strcmp(name, "shim_nhbell") == 0
       || std::strcmp(name, "shim_mark_synch") == 0
       || std::strcmp(name, "shim_delay_output") == 0
       || std::strcmp(name, "shim_resume_nhwindows") == 0
       || std::strcmp(name, "shim_suspend_nhwindows") == 0
       || std::strcmp(name, "shim_number_pad") == 0
       || std::strcmp(name, "shim_change_color") == 0
       || std::strcmp(name, "shim_change_background") == 0
       || std::strcmp(name, "shim_preference_update") == 0
       || std::strcmp(name, "shim_player_selection") == 0
       || std::strcmp(name, "shim_ctrl_nhwindow") == 0)
    {
        return;
    }

    if(return_ptr != nullptr && format != nullptr && format[0] != 'v')
    {
        std::fprintf(stderr, "Unhandled NetHack callback: %s (%s)\n",
                     name, format);
        std::memset(return_ptr, 0, sizeof(int));
    }
}

Json WindowAdapter::makeSnapshot() const
{
    Json snapshot = {
        {"schema_version", 1},
        {"game_id", game_id_},
        {"lifecycle", "waiting"},
        {"operation", nullptr},
        {"map", {
            {"width", MAP_WIDTH},
            {"height", MAP_HEIGHT},
            {"origin", {{"x", 1}, {"y", 0}}},
            {"rows", map_rows_},
            {"cursor", {{"x", cursor_x_}, {"y", cursor_y_}}},
        }},
        {"status", status_},
        {"messages", messages_},
        {"messages_truncated", messages_truncated_},
        {"inventory", inventory_},
        {"pending", nullptr},
        {"private_location", {{"depth", static_cast<int>(depth(&u.uz))}}},
    };
    return snapshot;
}

void WindowAdapter::reportTerminalResult(int how)
{
    if(active_adapter_ == nullptr)
    {
        return;
    }
    std::string reason = "failed";
    if(how == ASCENDED)
    {
        reason = "ascended";
    }
    else if(how == ESCAPED)
    {
        reason = "escaped";
    }
    else if(how == QUIT)
    {
        reason = "quit";
    }
    else if(how >= DIED && how < PANICKED)
    {
        reason = "died";
    }
    std::string error;
    if(!active_adapter_->channel_.send({
           {"type", "terminal_result"},
           {"game_id", active_adapter_->game_id_},
           {"native_how", how},
           {"end_reason", reason},
       }, error))
    {
        std::fprintf(stderr, "could not send terminal result: %s\n",
                     error.c_str());
    }
}

Json WindowAdapter::makePending(std::string kind, std::string source) const
{
    return {
        {"input_id", input_id_ + 1},
        {"kind", std::move(kind)},
        {"source", std::move(source)},
    };
}

Json WindowAdapter::waitForInput(Json pending)
{
    if(pending.value("kind", "") != "menu")
    {
        inventory_requested_ = false;
    }
    input_id_++;
    pending["input_id"] = input_id_;
    Json snapshot = makeSnapshot();
    snapshot["pending"] = pending;
    publishSnapshot(std::move(snapshot));

    while(true)
    {
        Json message;
        std::string error;
        if(!channel_.receive(message, error))
        {
            throw std::runtime_error(error);
        }
        if(message.value("type", "") == "shutdown")
        {
            throw std::runtime_error("worker shutdown requested");
        }
        if(message.value("type", "") != "input"
           || message.value("game_id", "") != game_id_
           || message.value("input_id", 0ULL) != input_id_)
        {
            continue;
        }
        inventory_["stale"] = true;
        return message.value("response", Json::object());
    }
}

void WindowAdapter::publishSnapshot(Json snapshot)
{
    Json message = {
        {"type", "snapshot"},
        {"ipc_version", IPC_VERSION},
        {"game_id", game_id_},
        {"sequence", ++sequence_},
        {"snapshot", std::move(snapshot)},
    };
    std::string error;
    if(!channel_.send(message, error))
    {
        throw std::runtime_error(error);
    }
}

void WindowAdapter::addMessage(const char* message)
{
    const std::string text = copyString(message);
    if(text.empty())
    {
        return;
    }
    static std::uint64_t message_id = 0;
    messages_.push_back({
        {"id", ++message_id},
        {"text", text},
    });
    if(messages_.size() > MAX_MESSAGES)
    {
        messages_.erase(messages_.begin());
        messages_truncated_ = true;
    }
}

void WindowAdapter::setIntegerReturn(void* return_ptr, int value) const
{
    if(return_ptr != nullptr)
    {
        *static_cast<int*>(return_ptr) = value;
    }
}

void WindowAdapter::setCharacterReturn(void* return_ptr, char value) const
{
    if(return_ptr != nullptr)
    {
        *static_cast<char*>(return_ptr) = value;
    }
}

void WindowAdapter::copyTextReturn(void* return_ptr,
                                   const std::string& text) const
{
    if(return_ptr != nullptr)
    {
        auto** destination = static_cast<char**>(return_ptr);
        if(text.empty())
        {
            static char empty[] = "";
            *destination = empty;
        }
        else
        {
            *destination = const_cast<char*>(text.c_str());
        }
    }
}

int WindowAdapter::resolveCommand(const std::string& command) const
{
    if(command.empty())
    {
        return -1;
    }
    int index = 0;
    for(const ext_func_tab* entry = extcmdlist;
        entry != nullptr && entry->ef_txt != nullptr; ++entry, ++index)
    {
        if(command == entry->ef_txt)
        {
            return index;
        }
    }
    return -1;
}

} // namespace nethack_mcp

extern "C" void nethack_mcp_end_result(int how)
{
    nethack_mcp::WindowAdapter::reportTerminalResult(how);
}