#include <random>
#include <games/io.h>
#include "card_manager.h"

#include "external/rapidjson/document.h"
#include "external/rapidjson/prettywriter.h"
#include "misc/eamuse.h"
#include "util/utils.h"
#include "util/fileutils.h"
#include "cfg/configurator.h"

using namespace rapidjson;


namespace overlay::windows {

    CardManager::CardManager(SpiceOverlay *overlay) : Window(overlay) {
        this->title = "Card Manager";
        this->init_size = ImVec2(420, 420);

        if (cfg::CONFIGURATOR_STANDALONE) {
            this->init_pos = ImVec2(40, 40);
        } else {
            this->init_pos = ImVec2(
                    ImGui::GetIO().DisplaySize.x / 2 - this->init_size.x / 2,
                    ImGui::GetIO().DisplaySize.y / 2 - this->init_size.y / 2);
        }

        this->toggle_button = games::OverlayButtons::ToggleCardManager;
        this->config_path = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools_card_manager.json";
        if (fileutils::file_exists(this->config_path)) {
            this->config_load();
        }
    }

    CardManager::~CardManager() {
    }

    void CardManager::build_content() {
        ImGui::SeparatorText("Selected card");
        build_card();
        ImGui::SeparatorText("Available cards");
        build_card_list();
        ImGui::Separator();
        ImGui::Spacing();
        build_footer();

        build_card_editor();
    }

    void CardManager::build_card() {
        ImGui::BeginDisabled(this->current_card == nullptr);
        // insert P1 button
        if (ImGui::Button("Insert P1")) {
            const auto card = this->current_card;
            uint8_t card_bin[8];
            if (card && card->id.length() == 16 && hex2bin(card->id.c_str(), card_bin)) {
                eamuse_card_insert(0, card_bin);
            }
        }

        // insert P2 button
        if (eamuse_get_game_keypads() > 1) {
            ImGui::SameLine();
            if (ImGui::Button("Insert P2")) {
                const auto card = this->current_card;
                uint8_t card_bin[8];
                if (card && card->id.length() == 16 && hex2bin(card->id.c_str(), card_bin)) {
                    eamuse_card_insert(1, card_bin);
                }
            }
        }

        // edit selected card
        ImGui::SameLine();
        if (ImGui::Button("Edit Card")) {
            open_card_editor();
        }

        ImGui::EndDisabled();

        ImGui::Spacing();

        // card ui
        if (this->current_card) {
            const auto card = this->current_card;
            const ImVec4 color(card->color[0], card->color[1], card->color[2], 1.f);
            float bg_luminance = (0.299f * card->color[0] + 0.587 * card->color[1] + 0.114 * card->color[2]);

            // text color
            ImVec4 text_color;
            if (0.5f < bg_luminance) {
                text_color = ImVec4(0.f, 0.f, 0.f, 1.f); // black
            } else {
                text_color = ImVec4(1.f, 1.f, 1.f, 1.f); // white
            }

            ImGui::PushStyleColor(ImGuiCol_Button, color);
            ImGui::PushStyleColor(ImGuiCol_ButtonActive, color);
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color);
            ImGui::PushStyleColor(ImGuiCol_Text, text_color);
            if (ImGui::Button(fmt::format(
                    "  {}\n  {}  ",
                    card->name.empty() ? "<blank>" : card->name.substr(0, 16),
                    card->id
                    ).c_str())) {

                open_card_editor();
            }
            ImGui::PopStyleColor(4);
        } else {
            ImGui::BeginDisabled();
            ImGui::Button("  <No card>  \n  xxxxxxxxxxxxxxxx  ");
            ImGui::EndDisabled();
            ImGui::PopStyleColor(4);
        }
    }

    void CardManager::open_card_editor() {
        if (this->current_card) {
            const auto card = this->current_card;
            strcpy_s(this->name_buffer, std::size(this->name_buffer), card->name.c_str());
            strcpy_s(this->card_buffer, std::size(this->card_buffer), card->id.c_str());
            this->color_buffer[0] = card->color[0];
            this->color_buffer[1] = card->color[1];
            this->color_buffer[2] = card->color[2];
            ImGui::OpenPopup("Card Editor");
        }
    }

    void CardManager::build_card_editor() {
        // new/edit card popup
        if (ImGui::BeginPopupModal("Card Editor", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
            // card ID field (only editable for new cards)
            ImGui::BeginDisabled(this->current_card);
            ImGui::InputTextWithHint("Card ID", "E0040123456789AB",
                    this->card_buffer,
                    std::size(this->card_buffer),
                    ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase);
            if (this->current_card == nullptr) {
                ImGui::SameLine();
                if (ImGui::Button("Generate")) {
                    generate_ea_card(this->card_buffer);
                }
            }
            ImGui::EndDisabled();

            // name field
            ImGui::InputTextWithHint("Card Name", "Main Card",
                    this->name_buffer, std::size(this->name_buffer));

            // color
            ImGui::ColorEdit3("Color", this->color_buffer, ImGuiColorEditFlags_DisplayHex);
            ImGui::SameLine();
            if (ImGui::Button("Random")) {
                generate_random_color();
            }

            ImGui::Separator();

            // add/update button
            ImGui::BeginDisabled(strlen(this->card_buffer) != 16);
            if (ImGui::Button(this->current_card ? "Update Card" : "Save Card")) {
                if (this->current_card) {
                    // update existing card
                    this->current_card->name = strtrim(this->name_buffer);
                    this->current_card->color[0] = this->color_buffer[0];
                    this->current_card->color[1] = this->color_buffer[1];
                    this->current_card->color[2] = this->color_buffer[2];
                    generate_search_string(this->current_card);
                } else {
                    // create a new card
                    CardEntry card {
                        .name = strtrim(this->name_buffer),
                        .id = std::string(this->card_buffer),
                        .color = {this->color_buffer[0], this->color_buffer[1], this->color_buffer[2]}
                    };
                    generate_search_string(&card);
                    this->cards.emplace_back(card);
                    
                    // mark this card as the selected one
                    this->current_card = &this->cards.back();
                }
                this->config_save();
                ImGui::CloseCurrentPopup();
            }
            ImGui::EndDisabled();

            // delete current card button
            if (this->current_card) {
                ImGui::SameLine();
                if (ImGui::Button("Delete Card")) {
                    std::erase_if(this->cards, [&](CardEntry &card) {
                        return &card == this->current_card;
                    });
                    this->current_card = nullptr;
                    this->config_save();
                    ImGui::CloseCurrentPopup();
                }
            }

            ImGui::SameLine();
            if (ImGui::Button("Cancel")) {
                ImGui::CloseCurrentPopup();
            }

            ImGui::EndPopup();
        }
    }

    void CardManager::build_card_list() {

        // search for card
        //
        // setting ImGuiInputTextFlags_CallbackCharFilter and pressing escape doesn't cause below 
        // to return true, making it necessary to provide a callback...
        ImGui::SetNextItemWidth(220);
        if (ImGui::InputTextWithHint("", "Type here to search..", &this->search_filter)) {
            this->search_filter_in_lower_case = strtolower(this->search_filter);
        }
        if (!this->search_filter.empty()) {
            ImGui::SameLine();
            if (ImGui::Button("Clear")) {
                this->search_filter.clear();
                this->search_filter_in_lower_case.clear();
            }
        } else {
            ImGui::SameLine();
            
            // move selected up/down the list
            ImGui::BeginDisabled(this->current_card == nullptr);
            ImGui::SameLine();
            if (ImGui::Button("Move Up")) {
                for (auto it = this->cards.begin(); it != this->cards.end(); ++it) {
                    if (&*it == this->current_card && it != this->cards.begin()) {
                        std::iter_swap(it, it - 1);
                        this->current_card = &*(it - 1);
                        this->config_dirty = true;
                        break;
                    }
                }
            }
            ImGui::SameLine();
            if (ImGui::Button("Move Down")) {
                for (auto it = this->cards.begin(); it != this->cards.end(); ++it) {
                    if (&*it == this->current_card && (it + 1) != this->cards.end()) {
                        std::iter_swap(it, it + 1);
                        this->current_card = &*(it + 1);
                        this->config_dirty = true;
                        break;
                    }
                }
            }
            ImGui::EndDisabled();
        }

        ImGui::Spacing();

        // cards list
        // use all available vertical space, minus height of buttons, minus separator
        if (ImGui::BeginChild(
                "cards",
                ImVec2(0, ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() - 8.f))) {
            for (auto &card : this->cards) {

                // get card name
                std::string card_name = card.id;
                if (!card.name.empty()) {
                    card_name += " - ";
                    card_name += card.name;
                }

                if (!this->search_filter_in_lower_case.empty() && !card.search_string.empty()) {
                    const bool matched =
                        card.search_string.find(this->search_filter_in_lower_case) != std::string::npos;

                    if (!matched) {
                        continue;
                    }
                }

                // draw entry
                ImGui::PushID(&card);

                ImVec4 color(card.color[0], card.color[1], card.color[2], 1.f);
                ImGui::PushStyleColor(ImGuiCol_Button, color);
                ImGui::PushStyleColor(ImGuiCol_ButtonActive, color);
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color);
                ImGui::SmallButton(" ");
                ImGui::PopStyleColor(3);

                ImGui::SameLine();
                if (ImGui::Selectable(card_name.c_str(), this->current_card == &card)) {
                    this->current_card = &card;
                }
                ImGui::PopID();
            }
        }
        ImGui::EndChild();
    }

    void CardManager::build_footer() {

        // add new card
        if (ImGui::Button("Add New Card")) {
            memset(this->name_buffer, 0, sizeof(this->name_buffer));
            memset(this->card_buffer, 0, sizeof(this->card_buffer));
            generate_random_color();
            this->current_card = nullptr;
            ImGui::OpenPopup("Card Editor");
        }

        // save button
        if (this->config_dirty) {
            ImGui::SameLine();
            if (ImGui::Button("Save")) {
                this->config_save();
            }
        }
    }

    void CardManager::config_load() {
        log_info("cardmanager", "loading config");

        // clear cards
        this->cards.clear();

        // read config file
        std::string config = fileutils::text_read(this->config_path);
        if (!config.empty()) {

            // parse document
            Document doc;
            doc.Parse(config.c_str());

            // check parse error
            auto error = doc.GetParseError();
            if (error) {
                log_warning("cardmanager", "config parse error: {}", error);
            }

            // verify root is a dict
            if (doc.IsObject()) {

                // find pages
                auto pages = doc.FindMember("pages");
                if (pages != doc.MemberEnd() && pages->value.IsArray()) {

                    // iterate pages
                    for (auto &page : pages->value.GetArray()) {
                        if (page.IsObject()) {

                            // get cards
                            auto cards = page.FindMember("cards");
                            if (cards != doc.MemberEnd() && cards->value.IsArray()) {

                                // iterate cards
                                for (auto &card : cards->value.GetArray()) {
                                    if (card.IsObject()) {

                                        // find attributes
                                        auto name = card.FindMember("name");
                                        if (name == doc.MemberEnd() || !name->value.IsString()) {
                                            log_warning("cardmanager", "card name not found");
                                            continue;
                                        }
                                        auto id = card.FindMember("id");
                                        if (id == doc.MemberEnd() || !id->value.IsString()) {
                                            log_warning("cardmanager", "card id not found");
                                            continue;
                                        }

                                        // save entry
                                        CardEntry entry {
                                                .name = name->value.GetString(),
                                                .id = id->value.GetString(),
                                        };
                                        generate_search_string(&entry);

                                        // optional color
                                        auto color = card.FindMember("color");
                                        if (color != doc.MemberEnd() && color->value.IsArray()) {
                                            auto c = color->value.GetArray();
                                            if (c.Size() == 3 && c[0].IsFloat()) {
                                                entry.color[0] = c[0].GetFloat();
                                                entry.color[1] = c[1].GetFloat();
                                                entry.color[2] = c[2].GetFloat();
                                            }
                                        }

                                        this->cards.emplace_back(entry);

                                    } else {
                                        log_warning("cardmanager", "card is not an object");
                                    }
                                }
                            } else {
                                log_warning("cardmanager", "cards not found or not an array");
                            }
                        } else {
                            log_warning("cardmanager", "page is not an object");
                        }
                    }
                } else {
                    log_warning("cardmanager", "pages not found or not an array");
                }
            }
        }
    }

    void CardManager::config_save() {
        log_info("cardmanager", "saving config");

        // create document
        Document doc;
        doc.Parse(
                "{"
                "  \"pages\": ["
                "    {"
                "      \"cards\": ["
                "      ]"
                "    }"
                "  ]"
                "}"
        );

        // check parse error
        auto error = doc.GetParseError();
        if (error) {
            log_warning("cardmanager", "template parse error: {}", error);
        }

        // add cards
        auto &cards = doc["pages"][0]["cards"];
        for (auto &entry : this->cards) {
            Value card(kObjectType);
            card.AddMember("name", StringRef(entry.name.c_str()), doc.GetAllocator());
            card.AddMember("id", StringRef(entry.id.c_str()), doc.GetAllocator());

            Value color(kArrayType);
            color.PushBack(entry.color[0], doc.GetAllocator());
            color.PushBack(entry.color[1], doc.GetAllocator());
            color.PushBack(entry.color[2], doc.GetAllocator());
            card.AddMember("color", color, doc.GetAllocator());

            cards.PushBack(card, doc.GetAllocator());
        }

        // build JSON; using pretty writer so people can manually edit it
        StringBuffer buffer;
        PrettyWriter<StringBuffer> writer(buffer);
        doc.Accept(writer);

        // save to file
        if (fileutils::text_write(this->config_path, buffer.GetString())) {
            this->config_dirty = false;
        } else {
            log_warning("cardmanager", "unable to save config file to {}", this->config_path.string());
        }
    }

    void CardManager::generate_search_string(CardEntry *card) {
        card->search_string = strtolower(card->name) + " " + strtolower(card->id);
    }

    void CardManager::generate_random_color() {
        // these are colors on a hue wheel, starting from red
        static const char colors[48][7] = {
            "FF0000","FF2000","FF4000","FF6000","FF8000","FFAA00","FFCC00","FFEE00",
            "FFFF00","DDFF00","CCFF00","AAFF00","80FF00","60FF00","40FF00","20FF00",
            "00FF00","00FF20","00FF40","00FF60","00FF80","00FFAA","00FFCC","00FFDD",
            "00FFFF","00DDFF","00CCFF","0099FF","0080FF","0060FF","0040FF","0020FF",
            "0000FF","2000FF","4000FF","6000FF","8000FF","AA00FF","CC00FF","DD00FF",
            "FF00FF","FF00EE","FF00CC","FF00AA","FF0080","FF0060","FF0040","FF0020"
            };

        std::random_device rd;
        std::mt19937 generator(rd());
        std::uniform_int_distribution<> uniform(12, 36);

        // skip, ignoring half the hue wheel close to current index
        static int index = 0;
        index = (index + uniform(generator)) % 48;

        const auto hex = colors[index];

        uint8_t r, g, b;
        std::sscanf(hex, "%02hhx%02hhx%02hhx", &r, &g, &b);
        this->color_buffer[0] = r / 255.f;
        this->color_buffer[1] = g / 255.f;
        this->color_buffer[2] = b / 255.f;
    }
}
