#pragma once

#include <Windows.h>
#include <imgui.h>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>

#include "../config/CheckboxValue.h"
#include "../gui/menu/menu.h"
#include "../util/i18n.h"

class EspModule;
class MinimapModule;
class PlayerModule;
class AmmoHudModule;
class WeaponModule;
class WorldModule;
#ifdef INCLUDE_AIMBOT
class AimbotModule;
#endif

namespace hotkeys
{
    enum class section
    {
        aimbot,
        esp,
        exploits
    };

    struct binding
    {
        std::string id;
        const char* label = "";
        section sec = section::esp;
        int key = 0;
        int* key_storage = nullptr;
        CheckboxValue* toggle = nullptr;
        std::function<void()> action;
        std::function<bool()> can_enable;
    };

    inline int& binding_key(binding& b)
    {
        return b.key_storage ? *b.key_storage : b.key;
    }

    inline int binding_key_value(const binding& b)
    {
        return b.key_storage ? *b.key_storage : b.key;
    }

    inline std::vector<binding>& all()
    {
        static std::vector<binding> bindings;
        return bindings;
    }

    inline int* waiting_for_key = nullptr;
    inline std::unordered_map<int, bool> key_pressed;

    inline std::string get_key_name(const int key_code)
    {
        if (key_code == 0) return i18n::tr("none_inf");
        if (key_code > 0 && key_code < 165) return menu::KeyNames[key_code];
        return "Key " + std::to_string(key_code);
    }

    inline void add_toggle(const std::string& id, const char* label, section sec, CheckboxValue* toggle,
                           const int default_key = 0, std::function<bool()> can_enable = nullptr)
    {
        binding b;
        b.id = id;
        b.label = label;
        b.sec = sec;
        b.key = default_key;
        b.toggle = toggle;
        b.can_enable = std::move(can_enable);
        all().push_back(std::move(b));
    }

    inline void add_action(const std::string& id, const char* label, section sec,
                           std::function<void()> action, const int default_key = 0)
    {
        binding b;
        b.id = id;
        b.label = label;
        b.sec = sec;
        b.key = default_key;
        b.action = std::move(action);
        all().push_back(std::move(b));
    }

    inline void add_keybind(const std::string& id, const char* label, section sec, int* key_var,
                            const int default_key = 0)
    {
        binding b;
        b.id = id;
        b.label = label;
        b.sec = sec;
        b.key_storage = key_var;
        b.key = default_key;
        if (key_var)
            *key_var = default_key;
        all().push_back(std::move(b));
    }

#ifdef INCLUDE_AIMBOT
    void register_aimbot(AimbotModule* aimbot);
#endif
    void register_esp(EspModule* esp);
    void register_minimap(MinimapModule* minimap);
    void register_player(PlayerModule* player);
    void register_weapon(WeaponModule* weapon);
    void register_world(WorldModule* world);
    void register_ammo_hud(AmmoHudModule* ammo_hud);

    inline void clear_bindings()
    {
        all().clear();
    }

    inline bool try_toggle(CheckboxValue* toggle, const std::function<bool()>& can_enable)
    {
        if (!toggle) return false;

        const bool enabling = !toggle->get_value();
        if (enabling)
        {
            if (toggle->is_disabled()) return false;
            if (can_enable && !can_enable()) return false;
        }

        toggle->set_value(!toggle->get_value());
        return true;
    }

    inline void tick()
    {
        if (waiting_for_key != nullptr) return;

        for (auto& b : all())
        {
            if (b.key_storage) continue;

            const int key_code = binding_key(b);
            if (key_code == 0) continue;

            if (GetAsyncKeyState(key_code) & 0x8000)
            {
                if (!key_pressed[key_code])
                {
                    if (b.toggle)
                        try_toggle(b.toggle, b.can_enable);
                    else if (b.action)
                        b.action();
                    key_pressed[key_code] = true;
                }
            }
            else
            {
                key_pressed[key_code] = false;
            }
        }
    }

    inline void draw_keybind_button(const char* label, int& key_var)
    {
        const float sf = menu::get_scale_factor();
        ImGui::TextColored(menu::with_alpha(menu::palette::txt, 0.9f), "%s", i18n::tr(label).c_str());

        constexpr float btn_width = 85.0f;
        ImGui::SameLine(ImGui::GetContentRegionAvail().x - btn_width * sf - 4.f * sf);

        std::string btn_text;
        if (waiting_for_key == &key_var)
            btn_text = "[ ... ]";
        else
            btn_text = "[ " + get_key_name(key_var) + " ]";

        ImGui::PushID(label);
        if (ImGui::Button(btn_text.c_str(), ImVec2(btn_width * sf, 0)))
            waiting_for_key = &key_var;
        ImGui::PopID();

        if (waiting_for_key == &key_var)
        {
            for (int i = 3; i < 256; ++i)
            {
                if (GetAsyncKeyState(i) & 0x8000)
                {
                    key_var = i == VK_ESCAPE ? 0 : i;
                    waiting_for_key = nullptr;
                    break;
                }
            }
        }
    }

    inline void draw_section_header(const char* title)
    {
        ImGui::Dummy(ImVec2(0, 6.f));
        ImGui::Separator();
        ImGui::TextColored(ImVec4(0.72f, 0.78f, 0.55f, 1.f), "%s", i18n::tr(title).c_str());
        ImGui::Dummy(ImVec2(0, 2.f));
    }

    inline void draw_bindings_for_section(const section sec)
    {
        for (auto& b : all())
        {
            if (b.sec != sec) continue;
            draw_keybind_button(b.label, binding_key(b));
        }
    }

    inline void draw_panel()
    {
#ifdef INCLUDE_AIMBOT
        draw_section_header("aimbot_sec");
        draw_bindings_for_section(section::aimbot);
#endif

        draw_section_header("esp_sec");
        draw_bindings_for_section(section::esp);

        draw_section_header("exploits_sec");
        draw_bindings_for_section(section::exploits);
    }

    inline std::string config_prefix()
    {
        return "HK:";
    }

    inline void load(const std::unordered_map<std::string, std::string>& values)
    {
        const std::string prefix = config_prefix();
        for (auto& b : all())
        {
            std::string lookup_id = b.id;
            if (lookup_id == "esp.show_loot_prices")
                lookup_id = "esp.show_prices";

            auto iter = values.find(prefix + lookup_id);
            if (iter == values.end())
                iter = values.find(prefix + b.id);
            if (iter == values.end()) continue;
            try
            {
                binding_key(b) = std::stoi(iter->second);
            }
            catch (...)
            {
            }
        }
    }

    inline void append_save(std::string& value)
    {
        const std::string prefix = config_prefix();
        for (const auto& b : all())
        {
            value.append(prefix);
            value.append(b.id);
            value.append(";");
            value.append(std::to_string(binding_key_value(b)));
            value.append(";");
        }
    }

    inline void append_defaults(std::string& value)
    {
        append_save(value);
    }
}