#include "EventManager.hpp"
#include "../Compositor.hpp"

#include <algorithm>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
using namespace Hyprutils::OS;

CEventManager::CEventManager() : m_socketFD(socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) {
    if (!m_socketFD.isValid()) {
        Log::logger->log(Log::ERR, "Couldn't start the Hyprland Socket 2. (1) IPC will not work.");
        return;
    }

    sockaddr_un SERVERADDRESS = {.sun_family = AF_UNIX};
    const auto  PATH          = g_pCompositor->m_instancePath + "/.socket2.sock";
    if (PATH.length() > sizeof(SERVERADDRESS.sun_path) - 1) {
        Log::logger->log(Log::ERR, "Socket2 path is too long. (2) IPC will not work.");
        return;
    }

    strncpy(SERVERADDRESS.sun_path, PATH.c_str(), sizeof(SERVERADDRESS.sun_path) - 1);

    if (bind(m_socketFD.get(), rc<sockaddr*>(&SERVERADDRESS), SUN_LEN(&SERVERADDRESS)) < 0) {
        Log::logger->log(Log::ERR, "Couldn't bind the Hyprland Socket 2. (3) IPC will not work.");
        return;
    }

    // 10 max queued.
    if (listen(m_socketFD.get(), 10) < 0) {
        Log::logger->log(Log::ERR, "Couldn't listen on the Hyprland Socket 2. (4) IPC will not work.");
        return;
    }

    m_eventSource = wl_event_loop_add_fd(g_pCompositor->m_wlEventLoop, m_socketFD.get(), WL_EVENT_READABLE, onClientEvent, nullptr);
}

CEventManager::~CEventManager() {
    for (const auto& client : m_clients) {
        wl_event_source_remove(client.eventSource);
    }

    if (m_eventSource != nullptr)
        wl_event_source_remove(m_eventSource);
}

int CEventManager::onServerEvent(int fd, uint32_t mask, void* data) {
    return g_pEventManager->onClientEvent(fd, mask);
}

int CEventManager::onClientEvent(int fd, uint32_t mask, void* data) {
    return g_pEventManager->onServerEvent(fd, mask);
}

int CEventManager::onServerEvent(int fd, uint32_t mask) {
    if (mask & WL_EVENT_ERROR || mask & WL_EVENT_HANGUP) {
        Log::logger->log(Log::ERR, "Socket2 hangup?? IPC broke");

        wl_event_source_remove(m_eventSource);
        m_eventSource = nullptr;
        m_socketFD.reset();

        return 0;
    }

    sockaddr_in     clientAddress;
    socklen_t       clientSize = sizeof(clientAddress);
    CFileDescriptor ACCEPTEDCONNECTION{accept4(m_socketFD.get(), rc<sockaddr*>(&clientAddress), &clientSize, SOCK_CLOEXEC | SOCK_NONBLOCK)};
    if (!ACCEPTEDCONNECTION.isValid()) {
        if (errno != EAGAIN) {
            Log::logger->log(Log::ERR, "Socket2 failed receiving connection, errno: {}", errno);
            wl_event_source_remove(m_eventSource);
            m_eventSource = nullptr;
            m_socketFD.reset();
        }

        return 0;
    }

    Log::logger->log(Log::DEBUG, "Socket2 accepted a new client at FD {}", ACCEPTEDCONNECTION.get());

    // add to event loop so we can close it when we need to
    auto* eventSource = wl_event_loop_add_fd(g_pCompositor->m_wlEventLoop, ACCEPTEDCONNECTION.get(), 0, onServerEvent, nullptr);
    m_clients.emplace_back(SClient{
        std::move(ACCEPTEDCONNECTION),
        {},
        eventSource,
    });

    return 0;
}

int CEventManager::onClientEvent(int fd, uint32_t mask) {
    if (mask & WL_EVENT_ERROR || mask & WL_EVENT_HANGUP) {
        Log::logger->log(Log::DEBUG, "Socket2 fd {} hung up", fd);
        removeClientByFD(fd);
        return 0;
    }

    if (mask & WL_EVENT_WRITABLE) {
        const auto CLIENTIT = findClientByFD(fd);

        if (!flushClient(*CLIENTIT))
            removeClientByFD(fd);
    }

    return 0;
}

std::vector<CEventManager::SClient>::iterator CEventManager::findClientByFD(int fd) {
    return std::ranges::find_if(m_clients, [fd](const auto& client) { return client.fd.get() == fd; });
}

std::vector<CEventManager::SClient>::iterator CEventManager::removeClientByFD(int fd) {
    const auto CLIENTIT = findClientByFD(fd);
    wl_event_source_remove(CLIENTIT->eventSource);

    return m_clients.erase(CLIENTIT);
}

std::string CEventManager::formatEvent(const SHyprIPCEvent& event) const {
    std::string_view data        = event.data;
    auto             eventString = std::format("{}>>{}\n", event.event, data.substr(0, 1024));
    std::replace(eventString.begin() + event.event.length() + 2, eventString.end() - 1, '\n', ' ');
    return eventString;
}

bool CEventManager::flushClient(SClient& client) {
    while (!client.events.empty()) {
        const auto& event     = client.events.front();
        const auto  REMAINING = event->length() - client.writeOffset;
        const auto  WRITTEN   = write(client.fd.get(), event->c_str() + client.writeOffset, REMAINING);

        if (WRITTEN > 0) {
            client.writeOffset += sc<size_t>(WRITTEN);
            if (client.writeOffset < event->length())
                return true;

            client.writeOffset = 0;
            client.events.erase(client.events.begin());
            continue;
        }

        if (WRITTEN < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
            return true;

        if (WRITTEN < 0 && errno == EINTR)
            continue;

        Log::logger->log(Log::ERR, "Socket2 fd {} failed writing event: {}", client.fd.get(), WRITTEN < 0 ? strerror(errno) : "write returned 0");
        return false;
    }

    wl_event_source_fd_update(client.eventSource, 0);
    return true;
}

void CEventManager::postEvent(const SHyprIPCEvent& event) {
    if (g_pCompositor->m_isShuttingDown) {
        Log::logger->log(Log::WARN, "Suppressed (shutting down) event of type {}, content: {}", event.event, event.data);
        return;
    }

    const size_t MAX_QUEUED_EVENTS = 64;
    auto         sharedEvent       = makeShared<std::string>(formatEvent(event));
    for (auto it = m_clients.begin(); it != m_clients.end();) {
        const auto QUEUESIZE = it->events.size();

        if (QUEUESIZE >= MAX_QUEUED_EVENTS) {
            Log::logger->log(Log::ERR, "Socket2 fd {} overflowed event queue, removing", it->fd.get());
            it = removeClientByFD(it->fd.get());
            continue;
        }

        it->events.push_back(sharedEvent);

        if (!flushClient(*it)) {
            it = removeClientByFD(it->fd.get());
            continue;
        }

        if (!it->events.empty() && QUEUESIZE == 0)
            wl_event_source_fd_update(it->eventSource, WL_EVENT_WRITABLE);

        ++it;
    }
}
