#include "AsyncResourceManager.hpp"

#include "./resources/TextCmdResource.hpp"
#include "../helpers/Log.hpp"
#include "../helpers/MiscFunctions.hpp"
#include "../core/hyprlock.hpp"
#include "../config/ConfigManager.hpp"
#include "../core/Egl.hpp"

#include <algorithm>
#include <cstdint>
#include <functional>
#include <sys/eventfd.h>
#include <sys/poll.h>

using namespace Hyprgraphics;
using namespace Hyprutils::OS;

static inline ResourceID scopeResourceID(uint8_t scope, size_t in) {
    return (in & ~0x0f) | scope;
}

ResourceID CAsyncResourceManager::resourceIDForTextRequest(const CTextResource::STextResourceData& s) {
    // TODO: Currently ignores the font string and resulting distribution is probably not perfect.
    const auto H1 = std::hash<std::string>{}(s.text);
    const auto H2 = std::hash<double>{}(s.color.asRgb().r);
    const auto H3 = std::hash<double>{}(s.color.asRgb().g) + s.fontSize;
    const auto H4 = std::hash<double>{}(s.color.asRgb().b) + s.align;

    return scopeResourceID(1, H1 ^ (H2 << 1) ^ (H3 << 2) ^ (H4 << 3));
}

ResourceID CAsyncResourceManager::resourceIDForTextCmdRequest(const CTextResource::STextResourceData& s, size_t revision) {
    return scopeResourceID(2, resourceIDForTextRequest(s) ^ (revision << 32));
}

ResourceID CAsyncResourceManager::resourceIDForImageRequest(const std::string& path, size_t revision) {
    return scopeResourceID(3, std::hash<std::string>{}(path) ^ (revision << 32));
}

ResourceID CAsyncResourceManager::resourceIDForScreencopy(const std::string& port) {
    return scopeResourceID(4, std::hash<std::string>{}(port));
}

ResourceID CAsyncResourceManager::requestText(const CTextResource::STextResourceData& params, const AWP<IWidget>& widget) {
    const auto RESOURCEID = resourceIDForTextRequest(params);
    if (request(RESOURCEID, widget)) {
        Log::logger->log(Log::TRACE, "Reusing text resource \"{}\" (resourceID: {})", params.text, RESOURCEID);
        return RESOURCEID;
    }

    auto                                 resource = makeAtomicShared<CTextResource>(CTextResource::STextResourceData{params});
    CAtomicSharedPointer<IAsyncResource> resourceGeneric{resource};

    Log::logger->log(Log::TRACE, "Requesting text resource \"{}\" (resourceID: {})", params.text, RESOURCEID);
    enqueue(RESOURCEID, resourceGeneric, widget);
    return RESOURCEID;
}

ResourceID CAsyncResourceManager::requestTextCmd(const CTextResource::STextResourceData& params, size_t revision, const AWP<IWidget>& widget) {
    const auto RESOURCEID = resourceIDForTextCmdRequest(params, revision);
    if (request(RESOURCEID, widget)) {
        Log::logger->log(Log::TRACE, "Reusing text cmd resource \"{}\" revision {} (resourceID: {})", params.text, revision, RESOURCEID);
        return RESOURCEID;
    }

    auto                                 resource = makeAtomicShared<CTextCmdResource>(CTextResource::STextResourceData{params});
    CAtomicSharedPointer<IAsyncResource> resourceGeneric{resource};

    Log::logger->log(Log::TRACE, "Requesting text cmd resource \"{}\" revision {} (resourceID: {})", params.text, revision, RESOURCEID);
    enqueue(RESOURCEID, resourceGeneric, widget);
    return RESOURCEID;
}

ResourceID CAsyncResourceManager::requestImage(const std::string& path, size_t revision, const AWP<IWidget>& widget) {
    const auto RESOURCEID = resourceIDForImageRequest(path, revision);
    if (request(RESOURCEID, widget)) {
        Log::logger->log(Log::TRACE, "Reusing image resource {} revision {} (resourceID: {})", path, revision, RESOURCEID);
        return RESOURCEID;
    }

    auto                                 resource = makeAtomicShared<CImageResource>(absolutePath(path, ""));
    CAtomicSharedPointer<IAsyncResource> resourceGeneric{resource};

    Log::logger->log(Log::TRACE, "Requesting image resource {} revision {} (resourceID: {})", path, revision, RESOURCEID);
    enqueue(RESOURCEID, resourceGeneric, widget);
    return RESOURCEID;
}

ASP<CTexture> CAsyncResourceManager::getAssetByID(size_t id) {
    if (!m_assets.contains(id))
        return nullptr;

    return m_assets[id].texture;
}

void CAsyncResourceManager::enqueueStaticAssets() {
    const auto CWIDGETS = g_pConfigManager->getWidgetConfigs();

    for (auto& c : CWIDGETS) {
        if (c.type == "background" || c.type == "image") {
            std::string path = std::any_cast<Hyprlang::STRING>(c.values.at("path"));

            if (path.empty() || path == "screenshot")
                continue;

            requestImage(path, 0, nullptr);
        }
    }
}

void CAsyncResourceManager::enqueueScreencopyFrames() {
    if (g_pHyprlock->m_vOutputs.empty())
        return;

    for (const auto& MON : g_pHyprlock->m_vOutputs) {
        m_scFrames.emplace_back(makeUnique<CScreencopyFrame>());
        auto* frame = m_scFrames.back().get();
        frame->capture(MON);
        m_assets.emplace(frame->m_resourceID, SPreloadedTexture{.texture = nullptr, .refs = 1});
    }
}

void CAsyncResourceManager::screencopyToTexture(const CScreencopyFrame& scFrame) {
    if (!scFrame.m_ready || !m_assets.contains(scFrame.m_resourceID)) {
        Log::logger->log(Log::ERR, "Bogus call to CAsyncResourceManager::screencopyToTexture. This is a bug!");
        return;
    }

    m_assets[scFrame.m_resourceID].texture = scFrame.m_asset;

    Log::logger->log(Log::TRACE, "Done sc frame {}", scFrame.m_resourceID);

    std::erase_if(m_scFrames, [&scFrame](const auto& f) { return f.get() == &scFrame; });

    if (m_scFrames.empty()) {
        Log::logger->log(Log::INFO, "Gathered all screencopy frames - removing dmabuf listeners");
        g_pHyprlock->removeDmabufListener();
    }
}

void CAsyncResourceManager::gatherInitialResources(wl_display* display) {
    const auto MAXDELAYMS    = 2000; // 2 Seconds
    const auto STARTGATHERTP = std::chrono::system_clock::now();

    m_gatheredEventfd = CFileDescriptor{eventfd(0, EFD_CLOEXEC)};

    int    fdcount = 1;
    pollfd pollfds[2];
    pollfds[0] = {
        .fd     = wl_display_get_fd(display),
        .events = POLLIN,
    };

    if (m_gatheredEventfd.isValid()) {
        pollfds[1] = {
            .fd     = m_gatheredEventfd.get(),
            .events = POLLIN,
        };

        fdcount++;
    }

    bool gathered = false;
    while (!gathered) {
        wl_display_flush(display);
        if (wl_display_prepare_read(display) == 0) {
            if (poll(pollfds, fdcount, /* 100ms timeout */ 100) < 0) {
                RASSERT(errno == EINTR, "[core] Polling fds failed with {}", errno);
                wl_display_cancel_read(display);
                continue;
            }
            wl_display_read_events(display);
            wl_display_dispatch_pending(display);
        } else {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            wl_display_dispatch(display);
        }

        g_pHyprlock->processTimers();

        if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - STARTGATHERTP).count() > MAXDELAYMS) {
            Log::logger->log(Log::WARN, "Gathering resources timed out after {} milliseconds. Backgrounds may be delayed and render `background:color` at first.", MAXDELAYMS);
            break;
        }

        gathered = m_resources.empty() && m_scFrames.empty();
    }

    Log::logger->log(Log::INFO, "Resources gathered after {} milliseconds",
                     std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - STARTGATHERTP).count());
}

bool CAsyncResourceManager::checkIdPresent(ResourceID id) {
    return m_assets.contains(id);
}

void CAsyncResourceManager::unload(ASP<CTexture> texture) {
    auto preload = std::ranges::find_if(m_assets, [texture](const auto& a) { return a.second.texture == texture; });
    if (preload == m_assets.end())
        return;

    preload->second.refs--;

    if (preload->second.refs == 0) {
        Log::logger->log(Log::TRACE, "Releasing resourceID: {}!", preload->first);
        m_assets.erase(preload->first);
    }
}

void CAsyncResourceManager::unloadById(ResourceID id) {
    if (!m_assets.contains(id))
        return;

    m_assets[id].refs--;

    if (m_assets[id].refs == 0) {
        Log::logger->log(Log::TRACE, "Releasing resourceID: {}!", id);
        m_assets.erase(id);
    }
}

bool CAsyncResourceManager::request(ResourceID id, const AWP<IWidget>& widget) {
    if (!m_assets.contains(id)) {
        // New asset!!
        m_assets.emplace(id, SPreloadedTexture{.texture = nullptr, .refs = 1});
        return false;
    }

    m_assets[id].refs++;

    if (m_assets[id].texture) {
        // Asset already present. Dispatch the asset callback function.
        const auto& TEXTURE = m_assets[id].texture;
        if (auto w = widget.lock()) {
            w->onAssetUpdate(id, TEXTURE);

            // TODO: add a centalized mechanism to render in one place in the event loop to avoid duplicate render calls
            g_pHyprlock->addTimer(std::chrono::milliseconds(0), [](auto, auto) { g_pHyprlock->renderAllOutputs(); }, nullptr);
        }
    } else if (widget) {
        // Asset currently in-flight. Add the widget reference to in order for the callback to get dispatched later.
        m_resourcesMutex.lock();
        if (!m_resources.contains(id)) {
            Log::logger->log(Log::ERR, "In-flight resourceID: {} not found! This is a bug.", id);
            m_resourcesMutex.unlock();
            return true;
        }
        m_resources[id].second.emplace_back(widget);
        m_resourcesMutex.unlock();
    }

    return true;
}

void CAsyncResourceManager::enqueue(ResourceID resourceID, const ASP<IAsyncResource>& resource, const AWP<IWidget>& widget) {
    m_gatherer.enqueue(resource);

    m_resourcesMutex.lock();
    if (m_resources.contains(resourceID))
        Log::logger->log(Log::ERR, "Resource already enqueued! This is a bug.");

    m_resources[resourceID] = {resource, {widget}};
    m_resourcesMutex.unlock();

    resource->m_events.finished.listenStatic([resourceID]() {
        if (!g_pHyprlock)
            return;

        g_pHyprlock->addTimer(
            std::chrono::milliseconds(0),
            [](auto, void* resourceID) {
                if (g_asyncResourceManager)
                    g_asyncResourceManager->onResourceFinished((size_t)resourceID);
            },
            (void*)resourceID);
    });
}

void CAsyncResourceManager::onResourceFinished(ResourceID id) {
    m_resourcesMutex.lock();
    if (!m_resources.contains(id)) {
        m_resourcesMutex.unlock();
        return;
    }

    const auto RESOURCE = m_resources[id].first;
    const auto WIDGETS  = m_resources[id].second;
    m_resources.erase(id);
    m_resourcesMutex.unlock();

    if (!m_assets.contains(id) || m_assets[id].refs == 0) // Not referenced? Drop it
        return;

    if (!RESOURCE || !RESOURCE->m_asset.cairoSurface)
        return;

    Log::logger->log(Log::TRACE, "Resource to texture id:{}", id);

    g_pEGL->makeCurrent(nullptr);

    const auto           texture = makeAtomicShared<CTexture>();

    const cairo_status_t SURFACESTATUS = (cairo_status_t)RESOURCE->m_asset.cairoSurface->status();
    const auto           CAIROFORMAT   = cairo_image_surface_get_format(RESOURCE->m_asset.cairoSurface->cairo());
    const GLint          glIFormat     = CAIROFORMAT == CAIRO_FORMAT_RGB96F ? GL_RGB32F : GL_RGBA;
    const GLint          glFormat      = CAIROFORMAT == CAIRO_FORMAT_RGB96F ? GL_RGB : GL_RGBA;
    const GLint          glType        = CAIROFORMAT == CAIRO_FORMAT_RGB96F ? GL_FLOAT : GL_UNSIGNED_BYTE;

    if (SURFACESTATUS != CAIRO_STATUS_SUCCESS) {
        Log::logger->log(Log::ERR, "resourceID: {} invalid ({})", id, cairo_status_to_string(SURFACESTATUS));
        texture->m_iType = TEXTURE_INVALID;
    }

    texture->m_vSize = RESOURCE->m_asset.pixelSize;
    texture->allocate();

    glBindTexture(GL_TEXTURE_2D, texture->m_iTexID);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    if (CAIROFORMAT != CAIRO_FORMAT_RGB96F) {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
    }
    glTexImage2D(GL_TEXTURE_2D, 0, glIFormat, texture->m_vSize.x, texture->m_vSize.y, 0, glFormat, glType, RESOURCE->m_asset.cairoSurface->data());

    m_assets[id].texture = texture;

    for (const auto& widget : WIDGETS) {
        if (auto w = widget.lock())
            w->onAssetUpdate(id, texture);
    }

    g_pHyprlock->renderAllOutputs();

    if (!m_gathered && !g_pHyprlock->m_bImmediateRender) {
        m_resourcesMutex.lock();
        if (m_resources.empty()) {
            m_gathered = true;
            if (m_gatheredEventfd.isValid())
                eventfd_write(m_gatheredEventfd.get(), 1);

            m_gatheredEventfd.reset();
        }
        m_resourcesMutex.unlock();
    }
}
