#include "content/views/view_store.hpp"
#include <hex/api/theme_manager.hpp>
#include <hex/api/achievement_manager.hpp>
#include <hex/api_urls.hpp>

#include <hex/api/content_registry/user_interface.hpp>
#include <hex/api/content_registry/settings.hpp>
#include <hex/api/events/events_interaction.hpp>

#include <popups/popup_notification.hpp>
#include <toasts/toast_notification.hpp>

#include <imgui.h>

#include <hex/helpers/utils.hpp>
#include <hex/helpers/crypto.hpp>
#include <hex/helpers/logger.hpp>
#include <hex/helpers/magic.hpp>
#include <hex/helpers/fs.hpp>
#include <hex/helpers/tar.hpp>

#include <filesystem>
#include <functional>
#include <nlohmann/json.hpp>

#include <wolv/io/file.hpp>

namespace hex::plugin::builtin {

    using namespace std::literals::string_literals;
    using namespace std::literals::chrono_literals;

    ViewStore::ViewStore() : View::Floating("hex.builtin.view.store.name", ICON_VS_EXTENSIONS) {
        ContentRegistry::UserInterface::addMenuItem({ "hex.builtin.menu.extras", "hex.builtin.view.store.name" }, ICON_VS_EXTENSIONS, 1000, Shortcut::None, [&, this] {
            if (m_requestStatus == RequestStatus::NotAttempted)
                this->refresh();

            this->getWindowOpenState() = true;
        });

        m_httpRequest.setTimeout(30'000);

        addCategory("hex.builtin.view.store.tab.patterns",     "patterns",      &paths::Patterns);
        addCategory("hex.builtin.view.store.tab.includes",     "includes",      &paths::PatternsInclude);
        addCategory("hex.builtin.view.store.tab.magic",        "magic",         &paths::Magic, []{
            magic::compile();
        });
        addCategory("hex.builtin.view.store.tab.nodes",        "nodes",         &paths::Nodes);
        addCategory("hex.builtin.view.store.tab.encodings",    "encodings",     &paths::Encodings);
        addCategory("hex.builtin.view.store.tab.disassemblers","disassemblers", &paths::Disassemblers);
        addCategory("hex.builtin.view.store.tab.constants",    "constants",     &paths::Constants);
        addCategory("hex.builtin.view.store.tab.themes",       "themes",        &paths::Themes, [this]{
            auto themeFile = wolv::io::File(m_downloadPath, wolv::io::File::Mode::Read);

            ThemeManager::addTheme(themeFile.readString());
        });
        addCategory("hex.builtin.view.store.tab.yara",         "yara",         &paths::Yara);

        TaskManager::doLater([this] {
            // Force update all installed items after an update so that there's no old and incompatible versions around anymore
            {
                const auto prevUpdateVersion = ContentRegistry::Settings::read<std::string>("hex.builtin.setting.general", "hex.builtin.setting.general.prev_launch_version", "");
                if (SemanticVersion(prevUpdateVersion) != ImHexApi::System::getImHexVersion()) {
                    updateAll();
                }
            }
        });
    }


    void updateEntryMetadata(StoreEntry &storeEntry, const StoreCategory &category) {
        // Check if file is installed already or has an update available
        for (const auto &folder : category.path->write()) {
            const auto path = folder / std::fs::path(storeEntry.fileName);

            if (wolv::io::fs::exists(path)) {
                storeEntry.installed = true;

                wolv::io::File file(path, wolv::io::File::Mode::Read);
                auto bytes = file.readVector();

                auto fileHash = crypt::sha256(bytes);

                // Compare installed file hash with hash of repo file
                if (std::vector(fileHash.begin(), fileHash.end()) != crypt::decode16(storeEntry.hash))
                    storeEntry.hasUpdate = true;

                storeEntry.system = !fs::isPathWritable(folder);
                return;
            }
        }

        storeEntry.installed = false;
        storeEntry.hasUpdate = false;
        storeEntry.system = false;
    }

    void ViewStore::drawTab(hex::plugin::builtin::StoreCategory &category) {
        if (ImGui::BeginTabItem(Lang(category.unlocalizedName))) {
            if (ImGui::BeginTable("##pattern_language", 4, ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_RowBg)) {
                ImGui::TableSetupScrollFreeze(0, 1);
                ImGui::TableSetupColumn("hex.builtin.view.store.row.name"_lang, ImGuiTableColumnFlags_WidthFixed);
                ImGui::TableSetupColumn("hex.builtin.view.store.row.description"_lang, ImGuiTableColumnFlags_None);
                ImGui::TableSetupColumn("hex.builtin.view.store.row.authors"_lang, ImGuiTableColumnFlags_WidthFixed);
                ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);

                ImGui::TableHeadersRow();

                u32 id = 1;
                for (auto &entry : category.entries) {
                    ImGui::TableNextRow();
                    ImGui::TableNextColumn();
                    ImGui::TextUnformatted(entry.name.c_str());
                    ImGui::TableNextColumn();
                    ImGui::TextUnformatted(entry.description.c_str());
                    if (ImGui::IsItemHovered()) {
                        ImGui::BeginTooltip();
                        ImGui::PushTextWrapPos(500);
                        ImGui::TextUnformatted(entry.description.c_str());
                        ImGui::PopTextWrapPos();
                        ImGui::EndTooltip();
                    }
                    ImGui::TableNextColumn();
                    // The space makes a padding in the UI
                    ImGuiExt::TextFormatted("{} ", wolv::util::combineStrings(entry.authors, ", "));
                    ImGui::TableNextColumn();

                    ImGui::PushID(id);
                    ImGui::BeginDisabled(m_updateAllTask.isRunning() || (m_download.valid() && m_download.wait_for(0s) != std::future_status::ready));
                    {
                        if (entry.downloading) {
                            if (m_download.valid() && m_download.wait_for(0s) == std::future_status::ready) {
                                this->handleDownloadFinished(category, entry);
                            }

                        } else {
                            ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));

                            if (entry.hasUpdate) {
                                if (ImGuiExt::DimmedIconButton(ICON_VS_DEBUG_RESTART, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
                                    entry.downloading = this->download(category.path, entry.fileName, entry.link);
                                }
                                ImGui::SetItemTooltip("%s", "hex.builtin.view.store.update"_lang.get());
                            } else if (entry.system) {
                                ImGui::BeginDisabled();
                                ImGuiExt::DimmedIconButton(ICON_VS_REMOVE, ImGui::GetStyleColorVec4(ImGuiCol_Text));
                                ImGui::EndDisabled();
                                if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
                                    ImGui::BeginTooltip();
                                    ImGui::TextUnformatted("hex.builtin.view.store.system.explanation"_lang);
                                    ImGui::EndTooltip();
                                }
                            } else if (!entry.installed) {
                                if (ImGuiExt::DimmedIconButton(ICON_VS_CLOUD_DOWNLOAD, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
                                    entry.downloading = this->download(category.path, entry.fileName, entry.link);
                                    AchievementManager::unlockAchievement("hex.builtin.achievement.misc", "hex.builtin.achievement.misc.download_from_store.name");
                                }
                                ImGui::SetItemTooltip("%s", "hex.builtin.view.store.download"_lang.get());
                            } else {
                                if (ImGuiExt::DimmedIconButton(ICON_VS_TRASH, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
                                    entry.installed = !this->remove(category.path, entry.fileName);
                                    // remove() will not update the entry to mark it as a system entry, so we do it manually
                                    updateEntryMetadata(entry, category);
                                }
                                ImGui::SetItemTooltip("%s", "hex.builtin.view.store.remove"_lang.get());
                            }
                            ImGui::PopStyleVar();
                        }
                    }
                    ImGui::EndDisabled();
                    ImGui::PopID();
                    id++;
                }

                ImGui::EndTable();
            }
            ImGui::EndTabItem();
        }
    }

    void ViewStore::drawStore() {
        ImGuiExt::Header("hex.builtin.view.store.desc"_lang, true);

        bool reloading = false;
        if (m_apiRequest.valid()) {
            if (m_apiRequest.wait_for(0s) != std::future_status::ready)
                reloading = true;
            else {
                try {
                    this->parseResponse();
                } catch (nlohmann::json::exception &e) {
                    log::error("Failed to parse store response: {}", e.what());
                }
            }
        }

        ImGui::BeginDisabled(reloading);
        if (ImGui::Button("hex.builtin.view.store.reload"_lang)) {
            this->refresh();
        }
        ImGui::EndDisabled();

        if (reloading) {
            ImGui::SameLine();
            ImGuiExt::TextSpinner("hex.builtin.view.store.loading"_lang);
        }

        // Align the button to the right
        ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - 25_scaled);
        ImGui::BeginDisabled(m_updateAllTask.isRunning() || m_updateCount == 0);
        if (ImGuiExt::IconButton(ICON_VS_CLOUD_DOWNLOAD, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
            this->updateAll();
        }
        ImGuiExt::InfoTooltip(fmt::format("hex.builtin.view.store.update_count"_lang, m_updateCount.load()).c_str());

        ImGui::EndDisabled();

        if (ImGui::BeginTabBar("storeTabs")) {
            for (auto &category : m_categories) {
                this->drawTab(category);
            }

            ImGui::EndTabBar();
        }

    }

    void ViewStore::refresh() {
        // Do not refresh if a refresh is already in progress
        if (m_requestStatus == RequestStatus::InProgress)
            return;
        m_requestStatus = RequestStatus::InProgress;

        for (auto &category : m_categories) {
            category.entries.clear();
        }

        m_httpRequest.setUrl(ImHexApiURL + "/store"s);
        m_apiRequest = m_httpRequest.execute();
    }

    void ViewStore::parseResponse() {
        const auto response = m_apiRequest.get();
        m_requestStatus = response.isSuccess() ? RequestStatus::Succeeded : RequestStatus::Failed;
        if (m_requestStatus == RequestStatus::Succeeded) {
            const auto json = nlohmann::json::parse(response.getData());

            auto parseStoreEntries = [](auto storeJson, StoreCategory &category) {
                // Check if the response handles the type of files
                if (storeJson.contains(category.requestName)) {

                    for (auto &entry : storeJson[category.requestName]) {

                        // Check if entry is valid
                        if (entry.contains("name") && entry.contains("desc") && entry.contains("authors") && entry.contains("file") && entry.contains("url") && entry.contains("hash") && entry.contains("folder")) {

                            // Parse entry
                            StoreEntry storeEntry = { entry["name"], entry["desc"], entry["authors"], entry["file"], HttpRequest::curlify(entry["url"]), entry["hash"], entry["folder"], false, false, false, false };

                            updateEntryMetadata(storeEntry, category);
                            category.entries.push_back(storeEntry);
                        }
                    }
                }

                std::sort(category.entries.begin(), category.entries.end(), [](const auto &lhs, const auto &rhs) {
                    return lhs.name < rhs.name;
                });
            };

            for (auto &category : m_categories) {
                parseStoreEntries(json, category);
            }

            m_updateCount = 0;
            for (auto &category : m_categories) {
                for (auto &entry : category.entries) {
                    if (entry.hasUpdate)
                        m_updateCount += 1;
                }
            }
        }
        m_apiRequest = {};
    }

    void ViewStore::drawContent() {
        if (m_requestStatus == RequestStatus::Failed)
            ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_ToolbarRed), "hex.builtin.view.store.netfailed"_lang);

        this->drawStore();
    }

    bool ViewStore::download(const paths::impl::DefaultPath *pathType, const std::string &fileName, const std::string &url) {
        bool downloading = false;
        for (const auto &folderPath : pathType->write()) {
            if (!fs::isPathWritable(folderPath))
                continue;

            // Verify that we write the file to the right folder
            // this is to prevent the filename from having elements like ../
            const auto fullPath = std::fs::absolute(folderPath / std::fs::path(fileName));
            const auto [folderIter, pathIter] = std::mismatch(folderPath.begin(), folderPath.end(), fullPath.begin());
            if (folderIter != folderPath.end()) {
                continue;
            }

            downloading = true;
            m_downloadPath = fullPath;

            m_httpRequest.setUrl(url);
            m_download = m_httpRequest.downloadFile(fullPath);
            break;
        }

        if (!downloading) {
            ui::ToastError::open("hex.builtin.view.store.download_error"_lang);
            return false;
        }

        return downloading;
    }

    bool ViewStore::remove(const paths::impl::DefaultPath *pathType, const std::string &fileName) {
        bool removed = true;
        for (const auto &path : pathType->write()) {
            const auto filePath = path / fileName;
            const auto folderPath = (path / std::fs::path(fileName).stem());

            wolv::io::fs::remove(filePath);
            wolv::io::fs::removeAll(folderPath);

            removed = removed && !wolv::io::fs::exists(filePath) && !wolv::io::fs::exists(folderPath);
            EventStoreContentRemoved::post(filePath);
        }

        return removed;
    }

    void ViewStore::updateAll() {
        m_updateAllTask = TaskManager::createTask("hex.builtin.task.updating_store", m_updateCount, [this](auto &task) {
            for (auto &category : m_categories) {
                for (auto &entry : category.entries) {
                    if (entry.hasUpdate) {
                        entry.downloading = this->download(category.path, entry.fileName, entry.link);
                        if (!m_download.valid())
                            continue;

                        m_download.wait();

                        while (m_download.valid() && m_download.wait_for(100ms) != std::future_status::ready) {
                            task.update();
                        }

                        entry.hasUpdate = false;
                        entry.downloading = false;

                        if (m_updateCount > 0)
                            m_updateCount -= 1;

                        task.increment();
                    }
                }
            }

            TaskManager::doLater([] {
                ContentRegistry::Settings::write<std::string>("hex.builtin.setting.general", "hex.builtin.setting.general.prev_launch_version", ImHexApi::System::getImHexVersion().get(false));
            });
        });
    }


    void ViewStore::addCategory(const UnlocalizedString &unlocalizedName, const std::string &requestName, const paths::impl::DefaultPath *path, std::function<void()> downloadCallback) {
        m_categories.push_back({ unlocalizedName, requestName, path, { }, std::move(downloadCallback) });
    }

    void ViewStore::handleDownloadFinished(const StoreCategory &category, StoreEntry &entry) {
        entry.downloading = false;

        auto response = m_download.get();
        if (response.isSuccess()) {
            if (entry.hasUpdate)
                m_updateCount -= 1;

            entry.installed = true;
            entry.hasUpdate = false;
            entry.system = false;

            if (entry.isFolder) {
                Tar tar(m_downloadPath, Tar::Mode::Read);
                tar.extractAll(m_downloadPath.parent_path() / m_downloadPath.stem());
                EventStoreContentDownloaded::post(m_downloadPath.parent_path() / m_downloadPath.stem());
            } else {
                EventStoreContentDownloaded::post(m_downloadPath);
            }

            category.downloadCallback();
        } else {
            log::error("Download failed! HTTP Code {}", response.getStatusCode());
        }

        m_download = {};
    }

    void ViewStore::drawHelpText() {
        ImGuiExt::TextFormattedWrapped("This view lets you download and update additional content for ImHex, such as pattern files, magic files, themes and more. All content is provided by the ImHex community and can be freely used within ImHex.");
    }

}