#include "Fifo.hpp"
#include "Compositor.hpp"
#include "core/Compositor.hpp"
#include "../helpers/Monitor.hpp"
#include "../event/EventBus.hpp"

CFifoResource::CFifoResource(UP<CWpFifoV1>&& resource_, SP<CWLSurfaceResource> surface) : m_resource(std::move(resource_)), m_surface(surface) {
    if UNLIKELY (!m_resource->resource())
        return;

    m_resource->setData(this);
    m_resource->setDestroy([this](CWpFifoV1* r) { PROTO::fifo->destroyResource(this); });
    m_resource->setOnDestroy([this](CWpFifoV1* r) { PROTO::fifo->destroyResource(this); });

    m_resource->setSetBarrier([this](CWpFifoV1* r) {
        if (!m_surface) {
            r->error(WP_FIFO_V1_ERROR_SURFACE_DESTROYED, "Surface was gone");
            return;
        }

        m_surface->m_pending.barrierSet        = true;
        m_surface->m_pending.updated.bits.fifo = true;
    });

    m_resource->setWaitBarrier([this](CWpFifoV1* r) {
        if (!m_surface) {
            r->error(WP_FIFO_V1_ERROR_SURFACE_DESTROYED, "Surface was gone");
            return;
        }

        if (!m_surface->m_current.barrierSet) {
            // that might mean an empty commit with a barrier_set alone
            static const auto PPEND = CConfigValue<Hyprlang::INT>("debug:fifo_pending_workaround");
            if (!m_surface->m_pending.fifoScheduled)
                m_surface->m_pending.fifoScheduled = checkMonitors(*PPEND);

            return;
        }

        m_surface->m_pending.surfaceLocked = true;
    });

    m_listeners.surfaceStateCommit = m_surface->m_events.stateCommit.listen([this](auto state) {
        if (!state || !state->surfaceLocked)
            return;

        static const auto PPEND = CConfigValue<Hyprlang::INT>("debug:fifo_pending_workaround");

        //#TODO:
        // this feels wrong, but if we have no pending frames, presented might never come because
        // we are waiting on the barrier to unlock and no damage is around.
        // unlock on timeout instead?
        if (!state->fifoScheduled)
            state->fifoScheduled = checkMonitors(*PPEND);

        if (!state->fifoScheduled)
            return;

        // only lock once its mapped.
        if (m_surface->m_mapped)
            m_surface->m_stateQueue.lock(state, LOCK_REASON_FIFO);
    });
}

CFifoResource::~CFifoResource() {
    ;
}

bool CFifoResource::good() {
    return m_resource->resource();
}

void CFifoResource::presented() {
    m_surface->m_current.barrierSet = false;
    m_surface->m_stateQueue.unlockFirst(LOCK_REASON_FIFO);
}

bool CFifoResource::checkMonitors(bool needsSchedule) {
    if (m_surface->m_enteredOutputs.empty() && m_surface->m_hlSurface) {
        for (auto& m : g_pCompositor->m_monitors) {
            if (!m || !m->m_enabled)
                continue;

            auto box = m_surface->m_hlSurface->getSurfaceBoxGlobal();
            if (box && !box->intersection({m->m_position, m->m_size}).empty()) {
                if (m->m_tearingState.activelyTearing)
                    return false; // dont fifo lock on tearing.

                if (needsSchedule)
                    g_pCompositor->scheduleFrameForMonitor(m, Aquamarine::IOutput::AQ_SCHEDULE_NEEDS_FRAME);
            }
        }
    } else {
        for (auto& m : m_surface->m_enteredOutputs) {
            if (!m)
                continue;

            if (m->m_tearingState.activelyTearing)
                return false; // dont fifo lock on tearing.

            if (needsSchedule)
                g_pCompositor->scheduleFrameForMonitor(m.lock(), Aquamarine::IOutput::AQ_SCHEDULE_NEEDS_FRAME);
        }
    }

    return true;
}

CFifoManagerResource::CFifoManagerResource(UP<CWpFifoManagerV1>&& resource_) : m_resource(std::move(resource_)) {
    if UNLIKELY (!m_resource->resource())
        return;

    m_resource->setDestroy([this](CWpFifoManagerV1* r) { PROTO::fifo->destroyResource(this); });
    m_resource->setOnDestroy([this](CWpFifoManagerV1* r) { PROTO::fifo->destroyResource(this); });

    m_resource->setGetFifo([](CWpFifoManagerV1* r, uint32_t id, wl_resource* surfResource) {
        if (!surfResource) {
            r->error(-1, "No resource for fifo");
            return;
        }

        auto surf = CWLSurfaceResource::fromResource(surfResource);

        if (!surf) {
            r->error(-1, "No surface for fifo");
            return;
        }

        if (surf->m_fifo) {
            r->error(WP_FIFO_MANAGER_V1_ERROR_ALREADY_EXISTS, "Surface already has a fifo");
            return;
        }

        const auto& RESOURCE = PROTO::fifo->m_fifos.emplace_back(makeUnique<CFifoResource>(makeUnique<CWpFifoV1>(r->client(), r->version(), id), surf));

        if (!RESOURCE->good()) {
            r->noMemory();
            PROTO::fifo->m_fifos.pop_back();
            return;
        }

        surf->m_fifo = RESOURCE;
        LOGM(Log::DEBUG, "New fifo at {:x} for surface {:x}", (uintptr_t)RESOURCE, (uintptr_t)surf.get());
    });
}

CFifoManagerResource::~CFifoManagerResource() {
    ;
}

bool CFifoManagerResource::good() {
    return m_resource->resource();
}

CFifoProtocol::CFifoProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {
    static auto P = Event::bus()->m_events.monitor.added.listen([this](PHLMONITOR M) {
        M->m_events.presented.listenStatic([this, m = PHLMONITORREF{M}]() {
            if (!m || !PROTO::fifo)
                return;

            onMonitorPresent(m.lock());
        });
    });
}

void CFifoProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) {
    const auto RESOURCE = m_managers.emplace_back(makeUnique<CFifoManagerResource>(makeUnique<CWpFifoManagerV1>(client, ver, id))).get();

    if (!RESOURCE->good()) {
        wl_client_post_no_memory(client);
        m_managers.pop_back();
        return;
    }
}

void CFifoProtocol::destroyResource(CFifoManagerResource* res) {
    std::erase_if(m_managers, [&](const auto& other) { return other.get() == res; });
}

void CFifoProtocol::destroyResource(CFifoResource* res) {
    std::erase_if(m_fifos, [&](const auto& other) { return other.get() == res; });
}

void CFifoProtocol::onMonitorPresent(PHLMONITOR m) {
    if (m->m_tearingState.activelyTearing)
        return; // fifo isnt locked on tearing.

    for (const auto& fifo : m_fifos) {
        if (!fifo->m_surface)
            continue;

        if (!fifo->m_surface->m_mapped) {
            fifo->presented();
            continue;
        }

        auto it = std::ranges::find_if(fifo->m_surface->m_enteredOutputs, [m](auto& mon) { return mon == m; });
        if (it != fifo->m_surface->m_enteredOutputs.end()) {
            fifo->presented();
            continue;
        }

        if (fifo->m_surface->m_hlSurface) {
            auto box = fifo->m_surface->m_hlSurface->getSurfaceBoxGlobal();
            if (box && !box->intersection({m->m_position, m->m_size}).empty()) {
                fifo->presented();
                continue;
            }
        }
    }
}
