#include "Fifo.hpp"
#include "Compositor.hpp"
#include "core/Compositor.hpp"
#include "core/Subcompositor.hpp"
#include "../output/Monitor.hpp"
#include "../event/EventBus.hpp"
#include "../state/MonitorState.hpp"
#include "../desktop/view/View.hpp"
#include "../desktop/view/types/AlphaModifiable.hpp"
#include "../render/Renderer.hpp"
#include <algorithm>
#include <hyprutils/memory/WeakPtr.hpp>

// what nvidia says about the empty extra barrier commit.
/*
 * If the window is not visible (occluded, monitor on standby,
 * etc), then we could be waiting for an indefinite amount of time
 * for the compositor to send a wp_presentation_feedback::presented
 * or discarded event.
 *
 * But, wp_fifo_v1 is required to unblock in finite time, so we can
 * send an extra dummy commit with a wp_fifo_v1::wait_barrier.
 *
 * If the window is visible, then the compositor will send a
 * presented event as normal, and if the window is not visible,
 * then the second commit will trigger a discarded event.
 *
 * Note that the compositor may trigger a discarded event
 * immediately, so we use wp_commit_timer_v1 above to try to
 * throttle things to a sane rate.
 *
 * Ugly as this is, Mesa relies on the same behavior, so it's
 * probably safe to treat this as the "intended" behavior.
*/

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_resource->setWaitBarrier([this](CWpFifoV1* r) {
        if (!m_surface) {
            r->error(WP_FIFO_V1_ERROR_SURFACE_DESTROYED, "Surface was gone");
            return;
        }

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

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

        if (!state->barrierSet && !state->barrierWait)
            return;

        // the barrier constraint must be ignored for a subsurface in synchronized mode.
        if (m_surface->m_role->role() == SURFACE_ROLE_SUBSURFACE) {
            const auto sub = dynamicPointerCast<CSubsurfaceRole>(m_surface->m_role);
            if (sub) {
                const auto subsurface = sub->m_subsurface.lock();
                if (subsurface && subsurface->m_sync)
                    return;
            }
        }

        // check if entered outputs yet, and they are not tearing.
        if (!checkMonitors())
            return;

        // only lock once its mapped and visible and actually has something waiting for a presentation.
        if (m_surface->m_mapped && m_surface->m_current.waitingOnPresentation) {
            bool shouldLock = false;
            if (state->barrierSet && state->barrierWait) {
                static const auto PINVIS = CConfigValue<Hyprlang::INT>("render:not_shown_fifo_lock");
                shouldLock               = *PINVIS == 0 || !m_surface->m_hlSurface; // always && unknown
                if (!shouldLock && m_surface->m_hlSurface) {
                    const auto& view = m_surface->m_hlSurface->view();
                    if (view) {
                        const auto& window        = view->type() == Desktop::View::VIEW_TYPE_WINDOW ? dynamicPointerCast<Desktop::View::CWindow>(view) : nullptr;
                        const auto  alphaModifier = dynamicPointerCast<Desktop::View::IAlphaModifiable>(view);
                        const bool  isVisible     = (view->mapped() && view->acceptsInput() && (!alphaModifier || alphaModifier->alphaNonZero()) && //
                                                     (!window || std::ranges::any_of(State::monitorState()->monitors(), [window](const auto& mon) {
                                                    return g_pHyprRenderer->shouldRenderWindow(window, mon);
                                                     })));
                        if (isVisible)
                            shouldLock = true;
                        else if (*PINVIS == 2) // never
                            shouldLock = false;
                        else if (window && window->m_ruleApplicator->renderUnfocused().valueOr(false))
                            shouldLock = false; // ignore render_unfocused
                        else
                            shouldLock = true;
                    } else
                        shouldLock = true;
                }
            }

            if (shouldLock) {
                state->updated.bits.fifo = true;
                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.waitingOnPresentation = false;
    m_surface->m_stateQueue.unlockFirst(LOCK_REASON_FIFO);
}

bool CFifoResource::checkMonitors() {
    bool allowFifo = false;
    if (m_surface->m_enteredOutputs.empty() && m_surface->m_hlSurface) {
        for (auto& m : State::monitorState()->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.

                allowFifo = true; // intersects.
            }
        }
    } else {
        for (auto& m : m_surface->m_enteredOutputs) {
            if (!m)
                continue;

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

            allowFifo = true;
        }
    }

    return allowFifo;
}

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.get(), (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}](const Time::steady_tp&) {
            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;
            }
        }
    }
}
