#include "content/views/view_achievements.hpp"

#include <hex/api/content_registry.hpp>
#include <hex/api/task_manager.hpp>

#include <cmath>

#include <imgui_internal.h>

namespace hex::plugin::builtin {

    ViewAchievements::ViewAchievements() : View::Floating("hex.builtin.view.achievements.name") {
        // Add achievements menu item to Extas menu
        ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.extras", "hex.builtin.view.achievements.name" }, 2600, Shortcut::None, [&, this] {
            this->getWindowOpenState() = true;
        });

        // Add newly unlocked achievements to the display queue
        EventManager::subscribe<EventAchievementUnlocked>(this, [this](const Achievement &achievement) {
            this->m_achievementUnlockQueue.push_back(&achievement);
        });

        EventManager::subscribe<RequestOpenWindow>(this, [this](const std::string &name) {
            if (name == "Achievements") {
                TaskManager::doLater([this] {
                    this->getWindowOpenState() = true;
                });
            }
        });

        // Load settings
        this->m_showPopup = ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.achievement_popup", true);
    }

    ViewAchievements::~ViewAchievements() {
        EventManager::unsubscribe<EventAchievementUnlocked>(this);
    }

    void drawAchievement(ImDrawList *drawList, const AchievementManager::AchievementNode *node, ImVec2 position) {
        const auto achievementSize = scaled({ 50, 50 });

        auto &achievement = *node->achievement;

        // Determine achievement border color based on unlock state
        const auto borderColor = [&] {
            if (achievement.isUnlocked())
                return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_AchievementUnlocked, 1.0F);
            else if (node->isUnlockable())
                return ImGui::GetColorU32(ImGuiCol_Button, 1.0F);
            else
                return ImGui::GetColorU32(ImGuiCol_PlotLines, 1.0F);
        }();

        // Determine achievement fill color based on unlock state
        const auto fillColor = [&] {
            if (achievement.isUnlocked())
                return ImGui::GetColorU32(ImGuiCol_FrameBg, 1.0F) | 0xFF000000;
            else if (node->isUnlockable()) {
                auto cycleProgress = sinf(float(ImGui::GetTime()) * 6.0F) * 0.5F + 0.5F;
                return (u32(ImColor(ImLerp(ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), ImGui::GetStyleColorVec4(ImGuiCol_Text), cycleProgress))) & 0x00FFFFFF) | 0x80000000;
            }
            else
                return ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5F);
        }();

        // Draw achievement background
        if (achievement.isUnlocked()) {
            drawList->AddRectFilled(position, position + achievementSize, fillColor, 5_scaled, 0);
            drawList->AddRect(position, position + achievementSize, borderColor, 5_scaled, 0, 2_scaled);
        } else {
            drawList->AddRectFilled(position, position + achievementSize, ImGui::GetColorU32(ImGuiCol_WindowBg) | 0xFF000000, 5_scaled, 0);
        }

        // Draw achievement icon if available
        if (const auto &icon = achievement.getIcon(); icon.isValid()) {
            ImVec2 iconSize;
            if (icon.getSize().x > icon.getSize().y) {
                iconSize.x = achievementSize.x;
                iconSize.y = iconSize.x / icon.getSize().x * icon.getSize().y;
            } else {
                iconSize.y = achievementSize.y;
                iconSize.x = iconSize.y / icon.getSize().y * icon.getSize().x;
            }

            iconSize *= 0.7F;

            ImVec2 margin = (achievementSize - iconSize) / 2.0F;
            drawList->AddImage(icon, position + margin, position + margin + iconSize);
        }

        // Dim achievement if it is not unlocked
        if (!achievement.isUnlocked()) {
            drawList->AddRectFilled(position, position + achievementSize, fillColor, 5_scaled, 0);
            drawList->AddRect(position, position + achievementSize, borderColor, 5_scaled, 0, 2_scaled);
        }

        auto tooltipPos = position + ImVec2(achievementSize.x, 0);
        auto tooltipSize = achievementSize * ImVec2(4, 0);

        // Draw achievement tooltip when hovering over it
        if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(position, position + achievementSize)) {
            ImGui::SetNextWindowPos(tooltipPos);
            ImGui::SetNextWindowSize(tooltipSize);
            if (ImGui::BeginTooltip()) {
                if (achievement.isBlacked() && !achievement.isUnlocked()) {
                    // Handle achievements that are blacked out
                    ImGui::TextUnformatted("[ ??? ]");
                } else {
                    // Handle regular achievements

                    ImGui::BeginDisabled(!achievement.isUnlocked());

                    // Draw achievement name
                    ImGui::TextUnformatted(Lang(achievement.getUnlocalizedName()));

                    // Draw progress bar if achievement has progress
                    if (auto requiredProgress = achievement.getRequiredProgress(); requiredProgress > 1) {
                        ImGui::ProgressBar(float(achievement.getProgress()) / float(requiredProgress + 1), ImVec2(achievementSize.x * 4, 5_scaled), "");
                    }

                    bool separator = false;

                    // Draw prompt to click on achievement if it has a click callback
                    if (achievement.getClickCallback() && !achievement.isUnlocked()) {
                        ImGui::Separator();
                        separator = true;

                        ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_AchievementUnlocked), "[ {} ]", Lang("hex.builtin.view.achievements.click"));
                    }

                    // Draw achievement description if available
                    if (const auto &desc = achievement.getUnlocalizedDescription(); !desc.empty()) {
                        if (!separator)
                            ImGui::Separator();
                        else
                            ImGui::NewLine();

                        ImGuiExt::TextFormattedWrapped("{}", Lang(desc));
                    }
                    ImGui::EndDisabled();
                }

                ImGui::EndTooltip();
            }

            // Handle achievement click
            if (!achievement.isUnlocked() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
                if (ImGui::GetIO().KeyShift) {
                    // Allow achievements to be unlocked in debug mode by shift-clicking them
                    #if defined (DEBUG)
                        AchievementManager::unlockAchievement(node->achievement->getUnlocalizedCategory(), node->achievement->getUnlocalizedName());
                    #endif
                } else {
                    // Trigger achievement click callback
                    if (auto clickCallback = achievement.getClickCallback(); clickCallback)
                        clickCallback(achievement);
                }
            }
        }
    }

    void drawOverlay(ImDrawList *drawList, ImVec2 windowMin, ImVec2 windowMax, const std::string &currCategory) {
        auto &achievements = AchievementManager::getAchievements()[currCategory];

        // Calculate number of achievements that have been unlocked
        const auto unlockedCount = std::count_if(achievements.begin(), achievements.end(), [](const auto &entry) {
            const auto &[name, achievement] = entry;
            return achievement->isUnlocked();
        });

        // Calculate number of invisible achievements
        const auto invisibleCount = std::count_if(achievements.begin(), achievements.end(), [](const auto &entry) {
            const auto &[name, achievement] = entry;
            return achievement->isInvisible() && !achievement->isUnlocked();
        });

        // Calculate number of visible achievements
        const auto visibleCount = achievements.size() - invisibleCount;

        // Construct number of unlocked achievements text
        auto unlockedText = hex::format("{}: {} / {}{}", "hex.builtin.view.achievements.unlocked_count"_lang, unlockedCount, visibleCount, invisibleCount > 0 ? "+" : " ");

        // Calculate overlay size
        auto &style = ImGui::GetStyle();
        auto overlaySize = ImGui::CalcTextSize(unlockedText.c_str()) + style.ItemSpacing + style.WindowPadding * 2.0F;
        auto padding = scaled({ 10, 10 });

        auto overlayPos = ImVec2(windowMax.x - overlaySize.x - padding.x, windowMin.y + padding.y);

        // Draw overlay background
        drawList->AddRectFilled(overlayPos, overlayPos + overlaySize, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.8F));
        drawList->AddRect(overlayPos, overlayPos + overlaySize, ImGui::GetColorU32(ImGuiCol_Border));

        // Draw overlay content
        ImGui::SetCursorScreenPos(overlayPos + padding);
        ImGui::BeginGroup();

        ImGui::TextUnformatted(unlockedText.c_str());

        ImGui::EndGroup();
    }

    void drawBackground(ImDrawList *drawList, ImVec2 min, ImVec2 max, ImVec2 offset) {
        const auto patternSize = scaled({ 10, 10 });

        const auto darkColor  = ImGui::GetColorU32(ImGuiCol_TableRowBg);
        const auto lightColor = ImGui::GetColorU32(ImGuiCol_TableRowBgAlt);

        // Draw a border around the entire background
        drawList->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border), 0.0F, 0, 1.0_scaled);

        // Draw a checkerboard pattern
        bool light = false;
        bool prevStart = false;
        for (float x = min.x + offset.x; x < max.x; x += i32(patternSize.x)) {
            if (prevStart == light)
                light = !light;
            prevStart = light;

            for (float y = min.y + offset.y; y < max.y; y += i32(patternSize.y)) {
                drawList->AddRectFilled({ x, y }, { x + patternSize.x, y + patternSize.y }, light ? lightColor : darkColor);
                light = !light;
            }
        }
    }

    ImVec2 ViewAchievements::drawAchievementTree(ImDrawList *drawList, const AchievementManager::AchievementNode * prevNode, const std::vector<AchievementManager::AchievementNode*> &nodes, ImVec2 position) {
        ImVec2 maxPos = position;

        // Loop over all available achievement nodes
        for (auto &node : nodes) {
            // If the achievement is invisible and not unlocked yet, don't draw anything
            if (node->achievement->isInvisible() && !node->achievement->isUnlocked())
                continue;

            // If the achievement has any visibility requirements, check if they are met
            if (!node->visibilityParents.empty()) {
                bool visible = true;

                // Check if all the visibility requirements are unlocked
                for (auto &parent : node->visibilityParents) {
                    if (!parent->achievement->isUnlocked()) {
                        visible = false;
                        break;
                    }
                }

                // If any of the visibility requirements are not unlocked, don't draw the achievement
                if (!visible)
                    continue;
            }

            drawList->ChannelsSetCurrent(1);

            // Check if the achievement has any parents
            if (prevNode != nullptr) {
                // Check if the parent achievement is in the same category
                if (prevNode->achievement->getUnlocalizedCategory() != node->achievement->getUnlocalizedCategory())
                    continue;

                auto start = prevNode->position + scaled({ 25, 25 });
                auto end = position + scaled({ 25, 25 });
                auto middle = ((start + end) / 2.0F) - scaled({ 50, 0 });

                const auto color = [prevNode]{
                    if (prevNode->achievement->isUnlocked())
                        return ImGui::GetColorU32(ImGuiCol_Text) | 0xFF000000;
                    else
                        return ImGui::GetColorU32(ImGuiCol_TextDisabled) | 0xFF000000;
                }();

                // Draw a bezier curve between the parent and child achievement
                drawList->AddBezierQuadratic(start, middle, end, color, 2_scaled);

                // Handle jumping to an achievement
                if (this->m_achievementToGoto != nullptr) {
                    if (this->m_achievementToGoto == node->achievement) {
                        this->m_offset = position - scaled({ 100, 100 });
                    }
                }
            }

            drawList->ChannelsSetCurrent(2);

            // Draw the achievement
            drawAchievement(drawList, node, position);

            // Adjust the position for the next achievement and continue drawing the achievement tree
            node->position = position;
            auto newMaxPos = drawAchievementTree(drawList, node, node->children, position + scaled({ 150, 0 }));
            if (newMaxPos.x > maxPos.x)
                maxPos.x = newMaxPos.x;
            if (newMaxPos.y > maxPos.y)
                maxPos.y = newMaxPos.y;

            position.y = maxPos.y + 100_scaled;
        }

        return maxPos;
    }

    void ViewAchievements::drawContent() {
        if (ImGui::BeginTabBar("##achievement_categories")) {
            auto &startNodes = AchievementManager::getAchievementStartNodes();

            // Get all achievement category names
            std::vector<std::string> categories;
            for (const auto &[categoryName, achievements] : startNodes) {
                categories.push_back(categoryName);
            }

            std::reverse(categories.begin(), categories.end());

            // Draw each individual achievement category
            for (const auto &categoryName : categories) {
                const auto &achievements = startNodes[categoryName];

                // Check if any achievements in the category are unlocked or unlockable
                bool visible = false;
                for (const auto &achievement : achievements) {
                    if (achievement->isUnlocked() || achievement->isUnlockable()) {
                        visible = true;
                        break;
                    }
                }

                // If all achievements in this category are invisible, don't draw it
                if (!visible)
                    continue;

                ImGuiTabItemFlags flags = ImGuiTabItemFlags_None;

                // Handle jumping to the category of an achievement
                if (this->m_achievementToGoto != nullptr) {
                    if (this->m_achievementToGoto->getUnlocalizedCategory() == categoryName) {
                        flags |= ImGuiTabItemFlags_SetSelected;
                    }
                }

                // Draw the achievement category
                if (ImGui::BeginTabItem(Lang(categoryName), nullptr, flags)) {
                    auto drawList = ImGui::GetWindowDrawList();

                    const auto cursorPos = ImGui::GetCursorPos();
                    const auto windowPos = ImGui::GetWindowPos() + ImVec2(0, cursorPos.y);
                    const auto windowSize = ImGui::GetWindowSize() - ImVec2(0, cursorPos.y);
                    const float borderSize = 20.0_scaled;

                    const auto windowPadding = ImGui::GetStyle().WindowPadding;
                    const auto innerWindowPos = windowPos + ImVec2(borderSize, borderSize);
                    const auto innerWindowSize = windowSize - ImVec2(borderSize * 2, borderSize * 2) - ImVec2(0, ImGui::GetTextLineHeightWithSpacing());

                    // Prevent the achievement tree from being drawn outside of the window
                    drawList->PushClipRect(innerWindowPos, innerWindowPos + innerWindowSize, true);

                    drawList->ChannelsSplit(4);

                    drawList->ChannelsSetCurrent(0);

                    // Draw achievement background
                    drawBackground(drawList, innerWindowPos, innerWindowPos + innerWindowSize, this->m_offset);

                    // Draw the achievement tree
                    auto maxPos = drawAchievementTree(drawList, nullptr, achievements, innerWindowPos + scaled({ 100, 100 }) + this->m_offset);

                    drawList->ChannelsSetCurrent(3);

                    // Draw the achievement overlay
                    drawOverlay(drawList, innerWindowPos, innerWindowPos + innerWindowSize, categoryName);

                    drawList->ChannelsMerge();

                    // Handle dragging the achievement tree around
                    if (ImGui::IsMouseHoveringRect(innerWindowPos, innerWindowPos + innerWindowSize)) {
                        auto dragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left);
                        this->m_offset += dragDelta;
                        ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left);
                    }

                    // Clamp the achievement tree to the window
                    this->m_offset = -ImClamp(-this->m_offset, { 0, 0 }, ImMax(maxPos - innerWindowPos - innerWindowSize, { 0, 0 }));

                    drawList->PopClipRect();

                    // Draw settings below the window
                    ImGui::SetCursorScreenPos(innerWindowPos + ImVec2(0, innerWindowSize.y + windowPadding.y));
                    ImGui::BeginGroup();
                    {
                        if (ImGui::Checkbox("Show popup", &this->m_showPopup))
                            ContentRegistry::Settings::write("hex.builtin.setting.interface", "hex.builtin.setting.interface.achievement_popup", this->m_showPopup);
                    }
                    ImGui::EndGroup();

                    ImGui::EndTabItem();
                }
            }

            ImGui::EndTabBar();
        }

        this->m_achievementToGoto = nullptr;
    }

    void ViewAchievements::drawAlwaysVisibleContent() {

        // Handle showing the achievement unlock popup
        if (this->m_achievementUnlockQueueTimer >= 0 && this->m_showPopup) {
            this->m_achievementUnlockQueueTimer -= ImGui::GetIO().DeltaTime;

            // Check if there's an achievement that can be drawn
            if (this->m_currAchievement != nullptr) {

                const ImVec2 windowSize = scaled({ 200, 55 });
                ImGui::SetNextWindowPos(ImHexApi::System::getMainWindowPosition() + ImVec2 { ImHexApi::System::getMainWindowSize().x - windowSize.x - 100_scaled, 0 });
                ImGui::SetNextWindowSize(windowSize);
                if (ImGui::Begin("##achievement_unlocked", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs)) {
                    ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());

                    // Draw unlock text
                    ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_AchievementUnlocked), "{}", "hex.builtin.view.achievements.unlocked"_lang);

                    // Draw achievement icon
                    ImGui::Image(this->m_currAchievement->getIcon(), scaled({ 20, 20 }));

                    ImGui::SameLine();
                    ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
                    ImGui::SameLine();

                    // Draw name of achievement
                    ImGuiExt::TextFormattedWrapped("{}", Lang(this->m_currAchievement->getUnlocalizedName()));

                    // Handle clicking on the popup
                    if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
                        // Open the achievement window and jump to the achievement
                        this->getWindowOpenState() = true;
                        this->m_achievementToGoto = this->m_currAchievement;
                    }
                }
                ImGui::End();
            }
        } else {
            // Reset the achievement unlock queue timer
            this->m_achievementUnlockQueueTimer = -1.0F;
            this->m_currAchievement = nullptr;

            // If there are more achievements to draw, draw the next one
            if (!this->m_achievementUnlockQueue.empty()) {
                this->m_currAchievement = this->m_achievementUnlockQueue.front();
                this->m_achievementUnlockQueue.pop_front();
                this->m_achievementUnlockQueueTimer = 5.0F;
            }
        }
    }

}