#include "PointerManager.hpp"
#include "../Compositor.hpp"
#include "../config/ConfigValue.hpp"
#include "../config/ConfigManager.hpp"
#include "../protocols/PointerGestures.hpp"
#include "../protocols/RelativePointer.hpp"
#include "../protocols/FractionalScale.hpp"
#include "../protocols/IdleNotify.hpp"
#include "../protocols/core/Compositor.hpp"
#include "../protocols/core/Seat.hpp"
#include "eventLoop/EventLoopManager.hpp"
#include "../render/pass/TexPassElement.hpp"
#include "../managers/input/InputManager.hpp"
#include "../managers/HookSystemManager.hpp"
#include "../render/Renderer.hpp"
#include "../render/OpenGL.hpp"
#include "SeatManager.hpp"
#include "../helpers/time/Time.hpp"
#include <cstring>
#include <gbm.h>
#include <cairo/cairo.h>
#include <hyprutils/utils/ScopeGuard.hpp>

using namespace Hyprutils::Utils;

CPointerManager::CPointerManager() {
    m_hooks.monitorAdded = g_pHookSystem->hookDynamic("monitorAdded", [this](void* self, SCallbackInfo& info, std::any data) {
        auto PMONITOR = std::any_cast<PHLMONITOR>(data);

        onMonitorLayoutChange();

        PMONITOR->m_events.modeChanged.listenStatic([this] { g_pEventLoopManager->doLater([this]() { onMonitorLayoutChange(); }); });
        PMONITOR->m_events.disconnect.listenStatic([this] { g_pEventLoopManager->doLater([this]() { onMonitorLayoutChange(); }); });
        PMONITOR->m_events.destroy.listenStatic([this] {
            if (g_pCompositor && !g_pCompositor->m_isShuttingDown)
                std::erase_if(m_monitorStates, [](const auto& other) { return other->monitor.expired(); });
        });
    });

    m_hooks.monitorPreRender = g_pHookSystem->hookDynamic("preMonitorCommit", [this](void* self, SCallbackInfo& info, std::any data) {
        auto state = stateFor(std::any_cast<PHLMONITOR>(data));
        if (!state)
            return;

        state->cursorRendered = false;
    });
}

void CPointerManager::lockSoftwareAll() {
    for (auto const& state : m_monitorStates)
        state->softwareLocks++;

    updateCursorBackend();
}

void CPointerManager::unlockSoftwareAll() {
    for (auto const& state : m_monitorStates)
        state->softwareLocks--;

    updateCursorBackend();
}

void CPointerManager::lockSoftwareForMonitor(PHLMONITOR mon) {
    auto const state = stateFor(mon);
    state->softwareLocks++;

    if (state->softwareLocks == 1)
        updateCursorBackend();
}

void CPointerManager::unlockSoftwareForMonitor(PHLMONITOR mon) {
    auto const state = stateFor(mon);
    state->softwareLocks--;
    if (state->softwareLocks < 0)
        state->softwareLocks = 0;

    if (state->softwareLocks == 0)
        updateCursorBackend();
}

bool CPointerManager::softwareLockedFor(PHLMONITOR mon) {
    auto const state = stateFor(mon);
    return state->softwareLocks > 0 || state->hardwareFailed;
}

Vector2D CPointerManager::position() {
    return m_pointerPos;
}

bool CPointerManager::hasCursor() {
    return m_currentCursorImage.pBuffer || m_currentCursorImage.surface;
}

SP<CPointerManager::SMonitorPointerState> CPointerManager::stateFor(PHLMONITOR mon) {
    auto it = std::ranges::find_if(m_monitorStates, [mon](const auto& other) { return other->monitor == mon; });
    if (it == m_monitorStates.end())
        return m_monitorStates.emplace_back(makeShared<CPointerManager::SMonitorPointerState>(mon));
    return *it;
}

void CPointerManager::setCursorBuffer(SP<Aquamarine::IBuffer> buf, const Vector2D& hotspot, const float& scale) {
    damageIfSoftware();
    if (buf == m_currentCursorImage.pBuffer) {
        if (hotspot != m_currentCursorImage.hotspot || scale != m_currentCursorImage.scale) {
            m_currentCursorImage.hotspot = hotspot;
            m_currentCursorImage.scale   = scale;
            updateCursorBackend();
            damageIfSoftware();
        }

        return;
    }

    resetCursorImage(false);

    if (buf) {
        m_currentCursorImage.size    = buf->size;
        m_currentCursorImage.pBuffer = buf;
    }

    m_currentCursorImage.hotspot = hotspot;
    m_currentCursorImage.scale   = scale;

    updateCursorBackend();
    damageIfSoftware();
}

void CPointerManager::setCursorSurface(SP<CWLSurface> surf, const Vector2D& hotspot) {
    damageIfSoftware();

    if (surf == m_currentCursorImage.surface) {
        if (hotspot != m_currentCursorImage.hotspot || (surf && surf->resource() ? surf->resource()->m_current.scale : 1.F) != m_currentCursorImage.scale) {
            m_currentCursorImage.hotspot = hotspot;
            m_currentCursorImage.scale   = surf && surf->resource() ? surf->resource()->m_current.scale : 1.F;
            updateCursorBackend();
            damageIfSoftware();
        }

        return;
    }

    resetCursorImage(false);

    if (surf) {
        m_currentCursorImage.surface = surf;
        m_currentCursorImage.scale   = surf->resource()->m_current.scale;

        surf->resource()->map();

        m_currentCursorImage.destroySurface = surf->m_events.destroy.listen([this] { resetCursorImage(); });
        m_currentCursorImage.commitSurface  = surf->resource()->m_events.commit.listen([this] {
            damageIfSoftware();
            m_currentCursorImage.size  = m_currentCursorImage.surface->resource()->m_current.texture ? m_currentCursorImage.surface->resource()->m_current.bufferSize : Vector2D{};
            m_currentCursorImage.scale = m_currentCursorImage.surface ? m_currentCursorImage.surface->resource()->m_current.scale : 1.F;
            recheckEnteredOutputs();
            updateCursorBackend();
            damageIfSoftware();
        });

        if (surf->resource()->m_current.texture) {
            m_currentCursorImage.size = surf->resource()->m_current.bufferSize;
            surf->resource()->frame(Time::steadyNow());
        }
    }

    m_currentCursorImage.hotspot = hotspot;

    recheckEnteredOutputs();
    updateCursorBackend();
    damageIfSoftware();
}

void CPointerManager::recheckEnteredOutputs() {
    if (!hasCursor())
        return;

    auto box = getCursorBoxGlobal();

    for (auto const& s : m_monitorStates) {
        if (s->monitor.expired() || s->monitor->isMirror() || !s->monitor->m_enabled)
            continue;

        const bool overlaps = box.overlaps(s->monitor->logicalBox());

        if (!s->entered && overlaps) {
            s->entered = true;

            if (!m_currentCursorImage.surface)
                continue;

            m_currentCursorImage.surface->resource()->enter(s->monitor.lock());
            PROTO::fractional->sendScale(m_currentCursorImage.surface->resource(), s->monitor->m_scale);
            g_pCompositor->setPreferredScaleForSurface(m_currentCursorImage.surface->resource(), s->monitor->m_scale);
        } else if (s->entered && !overlaps) {
            s->entered = false;

            // if we are using hw cursors, prevent
            // the cursor from being stuck at the last point.
            if (!s->hardwareFailed &&
                (s->monitor->m_output->getBackend()->capabilities() & Aquamarine::IBackendImplementation::eBackendCapabilities::AQ_BACKEND_CAPABILITY_POINTER))
                setHWCursorBuffer(s, nullptr);

            if (!m_currentCursorImage.surface)
                continue;

            m_currentCursorImage.surface->resource()->leave(s->monitor.lock());
        }
    }
}

void CPointerManager::resetCursorImage(bool apply) {
    damageIfSoftware();

    if (m_currentCursorImage.surface) {
        for (auto const& m : g_pCompositor->m_monitors) {
            m_currentCursorImage.surface->resource()->leave(m);
        }

        m_currentCursorImage.surface->resource()->unmap();

        m_currentCursorImage.destroySurface.reset();
        m_currentCursorImage.commitSurface.reset();
        m_currentCursorImage.surface.reset();
    } else if (m_currentCursorImage.pBuffer)
        m_currentCursorImage.pBuffer = nullptr;

    if (m_currentCursorImage.bufferTex)
        m_currentCursorImage.bufferTex = nullptr;

    m_currentCursorImage.scale   = 1.F;
    m_currentCursorImage.hotspot = {0, 0};

    for (auto const& s : m_monitorStates) {
        if (s->monitor.expired() || s->monitor->isMirror() || !s->monitor->m_enabled)
            continue;

        s->entered = false;
    }

    if (!apply)
        return;

    for (auto const& ms : m_monitorStates) {
        if (!ms->monitor || !ms->monitor->m_enabled || !ms->monitor->m_dpmsStatus) {
            Debug::log(TRACE, "Not updating hw cursors: disabled / dpms off display");
            continue;
        }

        if (ms->cursorFrontBuffer) {
            if (ms->monitor->m_output->getBackend()->capabilities() & Aquamarine::IBackendImplementation::eBackendCapabilities::AQ_BACKEND_CAPABILITY_POINTER)
                ms->monitor->m_output->setCursor(nullptr, {});
            ms->cursorFrontBuffer = nullptr;
        }
    }
}

void CPointerManager::updateCursorBackend() {
    const auto CURSORBOX = getCursorBoxGlobal();

    for (auto const& m : g_pCompositor->m_monitors) {
        if (!m->m_enabled || !m->m_dpmsStatus) {
            Debug::log(TRACE, "Not updating hw cursors: disabled / dpms off display");
            continue;
        }

        auto CROSSES = !m->logicalBox().intersection(CURSORBOX).empty();
        auto state   = stateFor(m);

        if (!CROSSES) {
            if (state->cursorFrontBuffer)
                setHWCursorBuffer(state, nullptr);

            continue;
        }

        if (state->softwareLocks > 0 || g_pConfigManager->shouldUseSoftwareCursors(m) || !attemptHardwareCursor(state)) {
            Debug::log(TRACE, "Output {} rejected hardware cursors, falling back to sw", m->m_name);
            state->box            = getCursorBoxLogicalForMonitor(state->monitor.lock());
            state->hardwareFailed = true;

            if (state->hwApplied)
                setHWCursorBuffer(state, nullptr);

            state->hwApplied = false;
            continue;
        }

        state->hardwareFailed = false;
    }
}

void CPointerManager::onCursorMoved() {
    if (!hasCursor())
        return;

    const auto CURSORBOX = getCursorBoxGlobal();
    bool       recalc    = false;

    for (auto const& m : g_pCompositor->m_monitors) {
        auto state = stateFor(m);

        state->box = getCursorBoxLogicalForMonitor(state->monitor.lock());

        auto CROSSES = !m->logicalBox().intersection(CURSORBOX).empty();

        if (!CROSSES && state->cursorFrontBuffer) {
            Debug::log(TRACE, "onCursorMoved for output {}: cursor left the viewport, removing it from the backend", m->m_name);
            setHWCursorBuffer(state, nullptr);
            continue;
        } else if (CROSSES && !state->cursorFrontBuffer) {
            Debug::log(TRACE, "onCursorMoved for output {}: cursor entered the output, but no front buffer, forcing recalc", m->m_name);
            recalc = true;
        }

        if (!state->entered)
            continue;

        CScopeGuard x([m] { m->onCursorMovedOnMonitor(); });

        if (state->hardwareFailed)
            continue;

        const auto CURSORPOS = getCursorPosForMonitor(m);
        m->m_output->moveCursor(CURSORPOS, m->shouldSkipScheduleFrameOnMouseEvent());

        state->monitor->m_scanoutNeedsCursorUpdate = true;
    }

    if (recalc)
        updateCursorBackend();
}

bool CPointerManager::attemptHardwareCursor(SP<CPointerManager::SMonitorPointerState> state) {
    auto output = state->monitor->m_output;

    if (!(output->getBackend()->capabilities() & Aquamarine::IBackendImplementation::eBackendCapabilities::AQ_BACKEND_CAPABILITY_POINTER))
        return false;

    const auto CURSORPOS = getCursorPosForMonitor(state->monitor.lock());
    state->monitor->m_output->moveCursor(CURSORPOS, state->monitor->shouldSkipScheduleFrameOnMouseEvent());

    auto texture = getCurrentCursorTexture();

    if (!texture) {
        Debug::log(TRACE, "[pointer] no texture for hw cursor -> hiding");
        setHWCursorBuffer(state, nullptr);
        return true;
    }

    auto buffer = renderHWCursorBuffer(state, texture);

    if (!buffer) {
        Debug::log(TRACE, "[pointer] hw cursor failed rendering");
        setHWCursorBuffer(state, nullptr);
        return false;
    }

    bool success = setHWCursorBuffer(state, buffer);

    if (!success) {
        Debug::log(TRACE, "[pointer] hw cursor failed applying, hiding");
        setHWCursorBuffer(state, nullptr);
        return false;
    } else
        state->hwApplied = true;

    return success;
}

bool CPointerManager::setHWCursorBuffer(SP<SMonitorPointerState> state, SP<Aquamarine::IBuffer> buf) {
    if (!(state->monitor->m_output->getBackend()->capabilities() & Aquamarine::IBackendImplementation::eBackendCapabilities::AQ_BACKEND_CAPABILITY_POINTER))
        return false;

    const auto HOTSPOT = transformedHotspot(state->monitor.lock());

    Debug::log(TRACE, "[pointer] hw transformed hotspot for {}: {}", state->monitor->m_name, HOTSPOT);

    if (!state->monitor->m_output->setCursor(buf, HOTSPOT))
        return false;

    state->cursorFrontBuffer = buf;

    if (!state->monitor->shouldSkipScheduleFrameOnMouseEvent())
        g_pCompositor->scheduleFrameForMonitor(state->monitor.lock(), Aquamarine::IOutput::AQ_SCHEDULE_CURSOR_SHAPE);

    state->monitor->m_scanoutNeedsCursorUpdate = true;

    return true;
}

SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager::SMonitorPointerState> state, SP<CTexture> texture) {
    auto        maxSize    = state->monitor->m_output->cursorPlaneSize();
    auto const& cursorSize = m_currentCursorImage.size;

    static auto PCPUBUFFER = CConfigValue<Hyprlang::INT>("cursor:use_cpu_buffer");

    const bool  shouldUseCpuBuffer = *PCPUBUFFER == 1 || (*PCPUBUFFER != 0 && g_pHyprRenderer->isNvidia());

    if (maxSize == Vector2D{})
        return nullptr;

    if (maxSize != Vector2D{-1, -1}) {
        if (cursorSize.x > maxSize.x || cursorSize.y > maxSize.y) {
            Debug::log(TRACE, "hardware cursor too big! {} > {}", m_currentCursorImage.size, maxSize);
            return nullptr;
        }
    } else
        maxSize = cursorSize;

    if (!state->monitor->m_cursorSwapchain || maxSize != state->monitor->m_cursorSwapchain->currentOptions().size ||
        shouldUseCpuBuffer != (state->monitor->m_cursorSwapchain->getAllocator()->type() != Aquamarine::AQ_ALLOCATOR_TYPE_GBM)) {

        if (!state->monitor->m_cursorSwapchain || shouldUseCpuBuffer != (state->monitor->m_cursorSwapchain->getAllocator()->type() != Aquamarine::AQ_ALLOCATOR_TYPE_GBM)) {

            auto allocator = state->monitor->m_output->getBackend()->preferredAllocator();
            if (shouldUseCpuBuffer) {
                for (const auto& a : state->monitor->m_output->getBackend()->getAllocators()) {
                    if (a->type() == Aquamarine::AQ_ALLOCATOR_TYPE_DRM_DUMB) {
                        allocator = a;
                        break;
                    }
                }
            }

            auto backend                      = state->monitor->m_output->getBackend();
            auto primary                      = backend->getPrimary();
            state->monitor->m_cursorSwapchain = Aquamarine::CSwapchain::create(allocator, primary ? primary.lock() : backend);
        }

        auto options     = state->monitor->m_cursorSwapchain->currentOptions();
        options.size     = maxSize;
        options.length   = 2;
        options.scanout  = true;
        options.cursor   = true;
        options.multigpu = state->monitor->m_output->getBackend()->preferredAllocator()->drmFD() != g_pCompositor->m_drmFD;
        // We do not set the format (unless shm). If it's unset (DRM_FORMAT_INVALID) then the swapchain will pick for us,
        // but if it's set, we don't wanna change it.
        if (shouldUseCpuBuffer)
            options.format = DRM_FORMAT_ARGB8888;

        if (!state->monitor->m_cursorSwapchain->reconfigure(options)) {
            Debug::log(TRACE, "Failed to reconfigure cursor swapchain");
            return nullptr;
        }
    }

    // if we already rendered the cursor, revert the swapchain to avoid rendering the cursor over
    // the current front buffer
    // this flag will be reset in the preRender hook, so when we commit this buffer to KMS
    if (state->cursorRendered)
        state->monitor->m_cursorSwapchain->rollback();

    state->cursorRendered = true;

    auto buf = state->monitor->m_cursorSwapchain->next(nullptr);
    if (!buf) {
        Debug::log(TRACE, "Failed to acquire a buffer from the cursor swapchain");
        return nullptr;
    }

    if (shouldUseCpuBuffer) {
        // get the texture data if available.
        auto texData = texture->dataCopy();
        if (texData.empty()) {
            if (m_currentCursorImage.surface && m_currentCursorImage.surface->resource()->m_role->role() == SURFACE_ROLE_CURSOR) {
                const auto SURFACE   = m_currentCursorImage.surface->resource();
                auto&      shmBuffer = CCursorSurfaceRole::cursorPixelData(SURFACE);

                bool       flipRB = false;

                if (SURFACE->m_current.texture) {
                    Debug::log(TRACE, "Cursor CPU surface: format {}, expecting AR24", NFormatUtils::drmFormatName(SURFACE->m_current.texture->m_drmFormat));
                    if (SURFACE->m_current.texture->m_drmFormat == DRM_FORMAT_ABGR8888) {
                        Debug::log(TRACE, "Cursor CPU surface format AB24, will flip. WARNING: this will break on big endian!");
                        flipRB = true;
                    } else if (SURFACE->m_current.texture->m_drmFormat != DRM_FORMAT_ARGB8888) {
                        Debug::log(TRACE, "Cursor CPU surface format rejected, falling back to sw");
                        return nullptr;
                    }
                }

                if (shmBuffer.data())
                    texData = shmBuffer;
                else {
                    texData.resize(texture->m_size.x * 4 * texture->m_size.y);
                    memset(texData.data(), 0x00, texData.size());
                }

                if (flipRB) {
                    for (size_t i = 0; i < shmBuffer.size(); i += 4) {
                        std::swap(shmBuffer[i], shmBuffer[i + 2]); // little-endian!!!!!!
                    }
                }
            } else {
                Debug::log(TRACE, "Cannot use dumb copy on dmabuf cursor buffers");
                return nullptr;
            }
        }

        // then, we just yeet it into the dumb buffer

        const auto DMABUF      = buf->dmabuf();
        auto [data, fmt, size] = buf->beginDataPtr(0);

        auto CAIROSURFACE = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, DMABUF.size.x, DMABUF.size.y);
        auto CAIRODATASURFACE =
            cairo_image_surface_create_for_data((unsigned char*)texData.data(), CAIRO_FORMAT_ARGB32, texture->m_size.x, texture->m_size.y, texture->m_size.x * 4);

        auto CAIRO = cairo_create(CAIROSURFACE);

        cairo_set_operator(CAIRO, CAIRO_OPERATOR_SOURCE);
        cairo_set_source_rgba(CAIRO, 0, 0, 0, 0);
        cairo_rectangle(CAIRO, 0, 0, texture->m_size.x, texture->m_size.y);
        cairo_fill(CAIRO);

        const auto PATTERNPRE = cairo_pattern_create_for_surface(CAIRODATASURFACE);
        cairo_pattern_set_filter(PATTERNPRE, CAIRO_FILTER_BILINEAR);
        cairo_matrix_t matrixPre;
        cairo_matrix_init_identity(&matrixPre);

        const auto TR = state->monitor->m_transform;

        // we need to scale the cursor to the right size, because it might not be (esp with XCursor)
        const auto SCALE = texture->m_size / (m_currentCursorImage.size / m_currentCursorImage.scale * state->monitor->m_scale);
        cairo_matrix_scale(&matrixPre, SCALE.x, SCALE.y);

        if (TR) {
            cairo_matrix_rotate(&matrixPre, M_PI_2 * (double)TR);

            // FIXME: this is wrong, and doesnt work for 5, 6 and 7. (flipped + rot)
            // cba to do it rn, does anyone fucking use that??
            if (TR >= WL_OUTPUT_TRANSFORM_FLIPPED) {
                cairo_matrix_scale(&matrixPre, -1, 1);
                cairo_matrix_translate(&matrixPre, -DMABUF.size.x, 0);
            }

            if (TR == 3 || TR == 7)
                cairo_matrix_translate(&matrixPre, -DMABUF.size.x, 0);
            else if (TR == 2 || TR == 6)
                cairo_matrix_translate(&matrixPre, -DMABUF.size.x, -DMABUF.size.y);
            else if (TR == 1 || TR == 5)
                cairo_matrix_translate(&matrixPre, 0, -DMABUF.size.y);
        }

        cairo_pattern_set_matrix(PATTERNPRE, &matrixPre);
        cairo_set_source(CAIRO, PATTERNPRE);
        cairo_paint(CAIRO);

        cairo_surface_flush(CAIROSURFACE);

        cairo_pattern_destroy(PATTERNPRE);

        memcpy(data, cairo_image_surface_get_data(CAIROSURFACE), (size_t)cairo_image_surface_get_height(CAIROSURFACE) * cairo_image_surface_get_stride(CAIROSURFACE));

        cairo_destroy(CAIRO);
        cairo_surface_destroy(CAIROSURFACE);
        cairo_surface_destroy(CAIRODATASURFACE);

        buf->endDataPtr();

        return buf;
    }

    g_pHyprRenderer->makeEGLCurrent();
    g_pHyprOpenGL->m_renderData.pMonitor = state->monitor;

    auto RBO = g_pHyprRenderer->getOrCreateRenderbuffer(buf, state->monitor->m_cursorSwapchain->currentOptions().format);
    if (!RBO) {
        Debug::log(TRACE, "Failed to create cursor RB with format {}, mod {}", buf->dmabuf().format, buf->dmabuf().modifier);
        return nullptr;
    }

    RBO->bind();

    g_pHyprOpenGL->beginSimple(state->monitor.lock(), {0, 0, INT16_MAX, INT16_MAX}, RBO);
    g_pHyprOpenGL->clear(CHyprColor{0.F, 0.F, 0.F, 0.F});

    CBox xbox = {{}, Vector2D{m_currentCursorImage.size / m_currentCursorImage.scale * state->monitor->m_scale}.round()};
    Debug::log(TRACE, "[pointer] monitor: {}, size: {}, hw buf: {}, scale: {:.2f}, monscale: {:.2f}, xbox: {}", state->monitor->m_name, m_currentCursorImage.size, cursorSize,
               m_currentCursorImage.scale, state->monitor->m_scale, xbox.size());

    g_pHyprOpenGL->renderTexture(texture, xbox, 1.F);

    g_pHyprOpenGL->end();
    g_pHyprOpenGL->m_renderData.pMonitor.reset();

    g_pHyprRenderer->onRenderbufferDestroy(RBO.get());

    return buf;
}

void CPointerManager::renderSoftwareCursorsFor(PHLMONITOR pMonitor, const Time::steady_tp& now, CRegion& damage, std::optional<Vector2D> overridePos, bool forceRender) {
    if (!hasCursor())
        return;

    auto state = stateFor(pMonitor);

    if (!state->hardwareFailed && state->softwareLocks == 0 && !forceRender) {
        if (m_currentCursorImage.surface)
            m_currentCursorImage.surface->resource()->frame(now);
        return;
    }

    // don't render cursor if forced but we are already using sw cursors for the monitor
    // otherwise we draw the cursor again for screencopy when using sw cursors
    if (forceRender && (state->hardwareFailed || state->softwareLocks != 0))
        return;

    auto box = state->box.copy();
    if (overridePos.has_value()) {
        box.x = overridePos->x;
        box.y = overridePos->y;

        box.translate(-m_currentCursorImage.hotspot);
    }

    if (box.intersection(CBox{{}, {pMonitor->m_size}}).empty())
        return;

    auto texture = getCurrentCursorTexture();
    if (!texture)
        return;

    box.scale(pMonitor->m_scale);
    box.x = std::round(box.x);
    box.y = std::round(box.y);

    CTexPassElement::SRenderData data;
    data.tex = texture;
    data.box = box.round();

    g_pHyprRenderer->m_renderPass.add(makeUnique<CTexPassElement>(std::move(data)));

    if (m_currentCursorImage.surface)
        m_currentCursorImage.surface->resource()->frame(now);
}

Vector2D CPointerManager::getCursorPosForMonitor(PHLMONITOR pMonitor) {
    return CBox{m_pointerPos - pMonitor->m_position, {0, 0}}
               .transform(wlTransformToHyprutils(invertTransform(pMonitor->m_transform)), pMonitor->m_transformedSize.x / pMonitor->m_scale,
                          pMonitor->m_transformedSize.y / pMonitor->m_scale)
               .pos() *
        pMonitor->m_scale;
}

Vector2D CPointerManager::transformedHotspot(PHLMONITOR pMonitor) {
    if (!pMonitor->m_cursorSwapchain)
        return {}; // doesn't matter, we have no hw cursor, and this is only for hw cursors

    return CBox{m_currentCursorImage.hotspot * pMonitor->m_scale, {0, 0}}
        .transform(wlTransformToHyprutils(invertTransform(pMonitor->m_transform)), pMonitor->m_cursorSwapchain->currentOptions().size.x,
                   pMonitor->m_cursorSwapchain->currentOptions().size.y)
        .pos();
}

CBox CPointerManager::getCursorBoxLogicalForMonitor(PHLMONITOR pMonitor) {
    return getCursorBoxGlobal().translate(-pMonitor->m_position);
}

CBox CPointerManager::getCursorBoxGlobal() {
    return CBox{m_pointerPos, m_currentCursorImage.size / m_currentCursorImage.scale}.translate(-m_currentCursorImage.hotspot);
}

Vector2D CPointerManager::closestValid(const Vector2D& pos) {
    static auto PADDING = CConfigValue<Hyprlang::INT>("cursor:hotspot_padding");

    auto        CURSOR_PADDING = std::clamp((int)*PADDING, 0, 100);
    CBox        hotBox         = {{pos.x - CURSOR_PADDING, pos.y - CURSOR_PADDING}, {2 * CURSOR_PADDING, 2 * CURSOR_PADDING}};

    //
    static auto INSIDE_LAYOUT = [this](const CBox& box) -> bool {
        for (auto const& b : m_currentMonitorLayout.monitorBoxes) {
            if (box.inside(b))
                return true;
        }
        return false;
    };

    static auto INSIDE_LAYOUT_COORD = [this](const Vector2D& vec) -> bool {
        for (auto const& b : m_currentMonitorLayout.monitorBoxes) {
            if (b.containsPoint(vec))
                return true;
        }
        return false;
    };

    static auto NEAREST_LAYOUT = [this](const Vector2D& vec) -> Vector2D {
        Vector2D leader;
        float    distanceSq = __FLT_MAX__;

        for (auto const& b : m_currentMonitorLayout.monitorBoxes) {
            auto p      = b.closestPoint(vec);
            auto distSq = p.distanceSq(vec);

            if (distSq < distanceSq) {
                leader     = p;
                distanceSq = distSq;
            }
        }

        if (distanceSq > 1337.69420e+20F)
            return {0, 0}; // ???

        return leader;
    };

    if (INSIDE_LAYOUT(hotBox))
        return pos;

    Vector2D leader = NEAREST_LAYOUT(pos);

    hotBox.x = leader.x - CURSOR_PADDING;
    hotBox.y = leader.y - CURSOR_PADDING;

    // push the hotbox around so that it fits in the layout

    if (!INSIDE_LAYOUT_COORD(hotBox.middle() + Vector2D{CURSOR_PADDING, CURSOR_PADDING})) {
        auto delta = NEAREST_LAYOUT(hotBox.middle() + Vector2D{CURSOR_PADDING, CURSOR_PADDING}) - (hotBox.middle() + Vector2D{CURSOR_PADDING, CURSOR_PADDING});
        hotBox.translate(delta);
    }

    if (!INSIDE_LAYOUT_COORD(hotBox.middle() - Vector2D{CURSOR_PADDING, CURSOR_PADDING})) {
        auto delta = NEAREST_LAYOUT(hotBox.middle() - Vector2D{CURSOR_PADDING, CURSOR_PADDING}) - (hotBox.middle() - Vector2D{CURSOR_PADDING, CURSOR_PADDING});
        hotBox.translate(delta);
    }

    if (!INSIDE_LAYOUT_COORD(hotBox.middle() + Vector2D{CURSOR_PADDING, -CURSOR_PADDING})) {
        auto delta = NEAREST_LAYOUT(hotBox.middle() + Vector2D{CURSOR_PADDING, -CURSOR_PADDING}) - (hotBox.middle() + Vector2D{CURSOR_PADDING, -CURSOR_PADDING});
        hotBox.translate(delta);
    }

    if (!INSIDE_LAYOUT_COORD(hotBox.middle() + Vector2D{-CURSOR_PADDING, CURSOR_PADDING})) {
        auto delta = NEAREST_LAYOUT(hotBox.middle() + Vector2D{-CURSOR_PADDING, CURSOR_PADDING}) - (hotBox.middle() + Vector2D{-CURSOR_PADDING, CURSOR_PADDING});
        hotBox.translate(delta);
    }

    return hotBox.middle();
}

void CPointerManager::damageIfSoftware() {
    auto b = getCursorBoxGlobal().expand(4);

    for (auto const& mw : m_monitorStates) {
        if (mw->monitor.expired() || !mw->monitor->m_output)
            continue;

        if ((mw->softwareLocks > 0 || mw->hardwareFailed || g_pConfigManager->shouldUseSoftwareCursors(mw->monitor.lock())) &&
            b.overlaps({mw->monitor->m_position, mw->monitor->m_size})) {
            g_pHyprRenderer->damageBox(b, mw->monitor->shouldSkipScheduleFrameOnMouseEvent());
            break;
        }
    }
}

void CPointerManager::warpTo(const Vector2D& logical) {
    damageIfSoftware();

    m_pointerPos = closestValid(logical);

    if (!g_pInputManager->isLocked()) {
        recheckEnteredOutputs();
        onCursorMoved();
    }

    damageIfSoftware();
}

void CPointerManager::move(const Vector2D& deltaLogical) {
    const auto oldPos = m_pointerPos;
    auto       newPos = oldPos + Vector2D{std::isnan(deltaLogical.x) ? 0.0 : deltaLogical.x, std::isnan(deltaLogical.y) ? 0.0 : deltaLogical.y};

    warpTo(newPos);
}

void CPointerManager::warpAbsolute(Vector2D abs, SP<IHID> dev) {
    if (!dev)
        return;

    if (!std::isnan(abs.x))
        abs.x = std::clamp(abs.x, 0.0, 1.0);
    if (!std::isnan(abs.y))
        abs.y = std::clamp(abs.y, 0.0, 1.0);

    // find x and y size of the entire space
    const auto& MONITORS = g_pCompositor->m_monitors;
    Vector2D    topLeft = MONITORS.at(0)->m_position, bottomRight = MONITORS.at(0)->m_position + MONITORS.at(0)->m_size;
    for (size_t i = 1; i < MONITORS.size(); ++i) {
        const auto EXTENT = MONITORS[i]->logicalBox().extent();
        const auto POS    = MONITORS[i]->logicalBox().pos();
        if (EXTENT.x > bottomRight.x)
            bottomRight.x = EXTENT.x;
        if (EXTENT.y > bottomRight.y)
            bottomRight.y = EXTENT.y;
        if (POS.x < topLeft.x)
            topLeft.x = POS.x;
        if (POS.y < topLeft.y)
            topLeft.y = POS.y;
    }
    CBox mappedArea = {topLeft, bottomRight - topLeft};

    auto outputMappedArea = [&mappedArea](const std::string& output) {
        if (output == "current") {
            if (const auto PLASTMONITOR = g_pCompositor->m_lastMonitor.lock(); PLASTMONITOR)
                return PLASTMONITOR->logicalBox();
        } else if (const auto PMONITOR = g_pCompositor->getMonitorFromString(output); PMONITOR)
            return PMONITOR->logicalBox();
        return mappedArea;
    };

    switch (dev->getType()) {
        case HID_TYPE_TABLET: {
            CTablet* TAB = reinterpret_cast<CTablet*>(dev.get());
            if (!TAB->m_boundOutput.empty()) {
                mappedArea = outputMappedArea(TAB->m_boundOutput);
                mappedArea.translate(TAB->m_boundBox.pos());
            } else if (TAB->m_absolutePos) {
                mappedArea.x = TAB->m_boundBox.x;
                mappedArea.y = TAB->m_boundBox.y;
            } else
                mappedArea.translate(TAB->m_boundBox.pos());

            if (!TAB->m_boundBox.empty()) {
                mappedArea.w = TAB->m_boundBox.w;
                mappedArea.h = TAB->m_boundBox.h;
            }
            break;
        }
        case HID_TYPE_TOUCH: {
            ITouch* TOUCH = reinterpret_cast<ITouch*>(dev.get());
            if (!TOUCH->m_boundOutput.empty())
                mappedArea = outputMappedArea(TOUCH->m_boundOutput);
            break;
        }
        case HID_TYPE_POINTER: {
            IPointer* POINTER = reinterpret_cast<IPointer*>(dev.get());
            if (!POINTER->m_boundOutput.empty())
                mappedArea = outputMappedArea(POINTER->m_boundOutput);
            break;
        }
        default: break;
    }

    damageIfSoftware();

    if (std::isnan(abs.x) || std::isnan(abs.y)) {
        m_pointerPos.x = std::isnan(abs.x) ? m_pointerPos.x : mappedArea.x + mappedArea.w * abs.x;
        m_pointerPos.y = std::isnan(abs.y) ? m_pointerPos.y : mappedArea.y + mappedArea.h * abs.y;
    } else
        m_pointerPos = mappedArea.pos() + mappedArea.size() * abs;

    onCursorMoved();
    recheckEnteredOutputs();

    damageIfSoftware();
}

void CPointerManager::onMonitorLayoutChange() {
    m_currentMonitorLayout.monitorBoxes.clear();
    for (auto const& m : g_pCompositor->m_monitors) {
        if (m->isMirror() || !m->m_enabled || !m->m_output)
            continue;

        m_currentMonitorLayout.monitorBoxes.emplace_back(m->m_position, m->m_size);
    }

    damageIfSoftware();

    m_pointerPos = closestValid(m_pointerPos);
    updateCursorBackend();
    recheckEnteredOutputs();

    damageIfSoftware();
}

SP<CTexture> CPointerManager::getCurrentCursorTexture() {
    if (!m_currentCursorImage.pBuffer && (!m_currentCursorImage.surface || !m_currentCursorImage.surface->resource()->m_current.texture))
        return nullptr;

    if (m_currentCursorImage.pBuffer) {
        if (!m_currentCursorImage.bufferTex)
            m_currentCursorImage.bufferTex = makeShared<CTexture>(m_currentCursorImage.pBuffer, true);
        return m_currentCursorImage.bufferTex;
    }

    return m_currentCursorImage.surface->resource()->m_current.texture;
}

void CPointerManager::attachPointer(SP<IPointer> pointer) {
    if (!pointer)
        return;

    static auto PMOUSEDPMS = CConfigValue<Hyprlang::INT>("misc:mouse_move_enables_dpms");

    //
    auto listener = m_pointerListeners.emplace_back(makeShared<SPointerListener>());

    listener->pointer = pointer;

    listener->destroy = pointer->m_events.destroy.listen([this] { detachPointer(nullptr); });
    listener->motion  = pointer->m_pointerEvents.motion.listen([](const IPointer::SMotionEvent& event) {
        g_pInputManager->onMouseMoved(event);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->motionAbsolute = pointer->m_pointerEvents.motionAbsolute.listen([](const IPointer::SMotionAbsoluteEvent& event) {
        g_pInputManager->onMouseWarp(event);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->button = pointer->m_pointerEvents.button.listen([](const IPointer::SButtonEvent& event) {
        g_pInputManager->onMouseButton(event);
        PROTO::idle->onActivity();
    });

    listener->axis = pointer->m_pointerEvents.axis.listen([](const IPointer::SAxisEvent& event) {
        g_pInputManager->onMouseWheel(event);
        PROTO::idle->onActivity();
    });

    listener->frame = pointer->m_pointerEvents.frame.listen([] {
        bool shouldSkip = false;
        if (!g_pSeatManager->m_mouse.expired() && g_pInputManager->isLocked()) {
            auto PMONITOR = g_pCompositor->m_lastMonitor.get();
            shouldSkip    = PMONITOR && PMONITOR->shouldSkipScheduleFrameOnMouseEvent();
        }
        g_pSeatManager->m_isPointerFrameSkipped = shouldSkip;
        if (!g_pSeatManager->m_isPointerFrameSkipped)
            g_pSeatManager->sendPointerFrame();
    });

    listener->swipeBegin = pointer->m_pointerEvents.swipeBegin.listen([](const IPointer::SSwipeBeginEvent& event) {
        g_pInputManager->onSwipeBegin(event);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->swipeEnd = pointer->m_pointerEvents.swipeEnd.listen([](const IPointer::SSwipeEndEvent& event) {
        g_pInputManager->onSwipeEnd(event);
        PROTO::idle->onActivity();
    });

    listener->swipeUpdate = pointer->m_pointerEvents.swipeUpdate.listen([](const IPointer::SSwipeUpdateEvent& event) {
        g_pInputManager->onSwipeUpdate(event);
        PROTO::idle->onActivity();
    });

    listener->pinchBegin = pointer->m_pointerEvents.pinchBegin.listen([](const IPointer::SPinchBeginEvent& event) {
        PROTO::pointerGestures->pinchBegin(event.timeMs, event.fingers);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->pinchEnd = pointer->m_pointerEvents.pinchEnd.listen([](const IPointer::SPinchEndEvent& event) {
        PROTO::pointerGestures->pinchEnd(event.timeMs, event.cancelled);
        PROTO::idle->onActivity();
    });

    listener->pinchUpdate = pointer->m_pointerEvents.pinchUpdate.listen([](const IPointer::SPinchUpdateEvent& event) {
        PROTO::pointerGestures->pinchUpdate(event.timeMs, event.delta, event.scale, event.rotation);
        PROTO::idle->onActivity();
    });

    listener->holdBegin = pointer->m_pointerEvents.holdBegin.listen([](const IPointer::SHoldBeginEvent& event) {
        PROTO::pointerGestures->holdBegin(event.timeMs, event.fingers);
        PROTO::idle->onActivity();
    });

    listener->holdEnd = pointer->m_pointerEvents.holdEnd.listen([](const IPointer::SHoldEndEvent& event) {
        PROTO::pointerGestures->holdEnd(event.timeMs, event.cancelled);
        PROTO::idle->onActivity();
    });

    Debug::log(LOG, "Attached pointer {} to global", pointer->m_hlName);
}

void CPointerManager::attachTouch(SP<ITouch> touch) {
    if (!touch)
        return;

    static auto PMOUSEDPMS = CConfigValue<Hyprlang::INT>("misc:mouse_move_enables_dpms");

    //
    auto listener = m_touchListeners.emplace_back(makeShared<STouchListener>());

    listener->touch = touch;

    listener->destroy = touch->m_events.destroy.listen([this] { detachTouch(nullptr); });

    listener->down = touch->m_touchEvents.down.listen([](const ITouch::SDownEvent& event) {
        g_pInputManager->onTouchDown(event);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->up = touch->m_touchEvents.up.listen([](const ITouch::SUpEvent& event) {
        g_pInputManager->onTouchUp(event);
        PROTO::idle->onActivity();
    });

    listener->motion = touch->m_touchEvents.motion.listen([](const ITouch::SMotionEvent& event) {
        g_pInputManager->onTouchMove(event);
        PROTO::idle->onActivity();
    });

    listener->cancel = touch->m_touchEvents.cancel.listen([] {
        //
    });

    listener->frame = touch->m_touchEvents.frame.listen([] { g_pSeatManager->sendTouchFrame(); });

    Debug::log(LOG, "Attached touch {} to global", touch->m_hlName);
}

void CPointerManager::attachTablet(SP<CTablet> tablet) {
    if (!tablet)
        return;

    static auto PMOUSEDPMS = CConfigValue<Hyprlang::INT>("misc:mouse_move_enables_dpms");

    //
    auto listener = m_tabletListeners.emplace_back(makeShared<STabletListener>());

    listener->tablet = tablet;

    listener->destroy = tablet->m_events.destroy.listen([this] { detachTablet(nullptr); });

    listener->axis = tablet->m_tabletEvents.axis.listen([](const CTablet::SAxisEvent& event) {
        g_pInputManager->onTabletAxis(event);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->proximity = tablet->m_tabletEvents.proximity.listen([](const CTablet::SProximityEvent& event) {
        g_pInputManager->onTabletProximity(event);
        PROTO::idle->onActivity();
    });

    listener->tip = tablet->m_tabletEvents.tip.listen([](const CTablet::STipEvent& event) {
        g_pInputManager->onTabletTip(event);

        PROTO::idle->onActivity();

        if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS)
            g_pKeybindManager->dpms("on");
    });

    listener->button = tablet->m_tabletEvents.button.listen([](const CTablet::SButtonEvent& event) {
        g_pInputManager->onTabletButton(event);
        PROTO::idle->onActivity();
    });
    // clang-format on

    Debug::log(LOG, "Attached tablet {} to global", tablet->m_hlName);
}

void CPointerManager::detachPointer(SP<IPointer> pointer) {
    std::erase_if(m_pointerListeners, [pointer](const auto& e) { return e->pointer.expired() || e->pointer == pointer; });
}

void CPointerManager::detachTouch(SP<ITouch> touch) {
    std::erase_if(m_touchListeners, [touch](const auto& e) { return e->touch.expired() || e->touch == touch; });
}

void CPointerManager::detachTablet(SP<CTablet> tablet) {
    std::erase_if(m_tabletListeners, [tablet](const auto& e) { return e->tablet.expired() || e->tablet == tablet; });
}

void CPointerManager::damageCursor(PHLMONITOR pMonitor) {
    for (auto const& mw : m_monitorStates) {
        if (mw->monitor != pMonitor)
            continue;

        auto b = getCursorBoxGlobal().intersection(pMonitor->logicalBox());

        if (b.empty())
            return;

        g_pHyprRenderer->damageBox(b);

        return;
    }
}

Vector2D CPointerManager::cursorSizeLogical() {
    return m_currentCursorImage.size / m_currentCursorImage.scale;
}

void CPointerManager::storeMovement(uint64_t time, const Vector2D& delta, const Vector2D& deltaUnaccel) {
    m_storedTime = time;
    m_storedDelta += delta;
    m_storedUnaccel += deltaUnaccel;
}

void CPointerManager::setStoredMovement(uint64_t time, const Vector2D& delta, const Vector2D& deltaUnaccel) {
    m_storedTime    = time;
    m_storedDelta   = delta;
    m_storedUnaccel = deltaUnaccel;
}

void CPointerManager::sendStoredMovement() {
    PROTO::relativePointer->sendRelativeMotion((uint64_t)m_storedTime * 1000, m_storedDelta, m_storedUnaccel);
    m_storedTime    = 0;
    m_storedDelta   = Vector2D{};
    m_storedUnaccel = Vector2D{};
}
