/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/
module;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <wayland-client.h>
#include <wayland-client-protocol.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <fcntl.h>
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#endif
module Crafter.Graphics;
import std;

using namespace Crafter;

#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
namespace {
    // ─── write path ──────────────────────────────────────────────────
    //
    // Heap-allocated state attached to each wl_data_source via its
    // user_data slot. The text must outlive the source (the compositor
    // can call `send` minutes after we set the selection, every time a
    // remote app pastes), so we own it here and free it from
    // `OnSourceCancelled` — which is the only callback the compositor
    // guarantees will fire when the source is no longer needed (when
    // the selection is replaced, or on app exit).
    struct Held {
        std::string text;
    };

    void OnSourceTarget(void*, wl_data_source*, const char*) {}

    void OnSourceSend(void* data, wl_data_source*,
                      const char* /*mime_type*/, std::int32_t fd) {
        // We only ever advertise text MIME types, so any negotiated
        // type maps to the same UTF-8 buffer. `send` may fire multiple
        // times across the lifetime of one selection (each paste is a
        // fresh fd), so we must not consume `text` here.
        Held* h = static_cast<Held*>(data);
        const char*  p   = h->text.data();
        std::size_t  rem = h->text.size();
        while (rem > 0) {
            const ssize_t w = ::write(fd, p, rem);
            if (w < 0) {
                if (errno == EINTR) continue;
                break;          // pipe closed; remote gave up reading
            }
            if (w == 0) break;
            p   += w;
            rem -= static_cast<std::size_t>(w);
        }
        ::close(fd);
    }

    void OnSourceCancelled(void* data, wl_data_source* source) {
        // Selection was replaced (someone else copied) or the app is
        // exiting. Either way, this is our hook to release the held
        // text and the source itself.
        delete static_cast<Held*>(data);
        wl_data_source_destroy(source);
    }

    // Drag-and-drop only callbacks. We never start a DnD action, so
    // these can't fire — but the listener struct's size is fixed at
    // the v3 shape, and wayland-client uses the struct's slots
    // directly. Stubs are required.
    void OnSourceDndDropPerformed(void*, wl_data_source*) {}
    void OnSourceDndFinished(void*, wl_data_source*) {}
    void OnSourceAction(void*, wl_data_source*, std::uint32_t) {}

    constexpr wl_data_source_listener kSourceListener = {
        .target              = OnSourceTarget,
        .send                = OnSourceSend,
        .cancelled           = OnSourceCancelled,
        .dnd_drop_performed  = OnSourceDndDropPerformed,
        .dnd_finished        = OnSourceDndFinished,
        .action              = OnSourceAction,
    };

    // ─── read path ───────────────────────────────────────────────────
    //
    // The compositor announces every clipboard selection (and every
    // drag-and-drop) via a `wl_data_offer` we have to register a
    // listener on to learn its advertised mime types. The lifecycle:
    //
    //   1. wl_data_device.data_offer  → fresh wl_data_offer ID. We
    //      attach an offer listener so we receive the mime list.
    //   2. wl_data_offer.offer (N×)   → one event per advertised mime.
    //   3. wl_data_device.selection   → the offer becomes "the
    //      clipboard". Replaces (and invalidates) the previous one.
    //      An offer parameter of nullptr means the clipboard was
    //      cleared.
    //
    // DnD offers travel through the same data_offer event and end up
    // delivered via `enter`/`leave`/`drop`. We don't implement DnD,
    // but we still need to destroy those offers so we don't leak the
    // mime vector attached to them — we stash the most recent DnD
    // offer on enter and destroy it on leave/drop.
    struct OfferState {
        std::vector<std::string> mimes;
    };

    // Map keyed on raw wl_data_offer pointer. The compositor hands the
    // same pointer back in subsequent events, so identity is stable.
    std::unordered_map<wl_data_offer*, OfferState> g_offers;
    wl_data_offer* g_selectionOffer = nullptr;
    wl_data_offer* g_dndOffer = nullptr;
    bool g_listenerAttached = false;

    void DestroyOffer(wl_data_offer* offer) {
        if (offer == nullptr) return;
        g_offers.erase(offer);
        wl_data_offer_destroy(offer);
    }

    void OnOfferOffer(void*, wl_data_offer* offer, const char* mime) {
        auto it = g_offers.find(offer);
        if (it != g_offers.end()) it->second.mimes.emplace_back(mime);
    }
    void OnOfferSourceActions(void*, wl_data_offer*, std::uint32_t) {}
    void OnOfferAction(void*, wl_data_offer*, std::uint32_t) {}

    constexpr wl_data_offer_listener kOfferListener = {
        .offer          = OnOfferOffer,
        .source_actions = OnOfferSourceActions,
        .action         = OnOfferAction,
    };

    void OnDeviceDataOffer(void*, wl_data_device*, wl_data_offer* offer) {
        // First event in the sequence — register so we hear the mime list.
        // The offer can still end up unused (DnD that doesn't enter our
        // surface) but we always destroy it later in selection or
        // leave/drop.
        g_offers.emplace(offer, OfferState{});
        wl_data_offer_add_listener(offer, &kOfferListener, nullptr);
    }
    void OnDeviceEnter(void*, wl_data_device*, std::uint32_t,
                       wl_surface*, wl_fixed_t, wl_fixed_t,
                       wl_data_offer* offer) {
        // DnD started over our surface. We never accept, so we just
        // hold the offer until leave/drop and free it then.
        g_dndOffer = offer;
    }
    void OnDeviceLeave(void*, wl_data_device*) {
        DestroyOffer(g_dndOffer);
        g_dndOffer = nullptr;
    }
    void OnDeviceMotion(void*, wl_data_device*, std::uint32_t,
                        wl_fixed_t, wl_fixed_t) {}
    void OnDeviceDrop(void*, wl_data_device*) {
        DestroyOffer(g_dndOffer);
        g_dndOffer = nullptr;
    }
    void OnDeviceSelection(void*, wl_data_device*, wl_data_offer* offer) {
        // Selection changed; previous selection offer is invalid.
        // `offer == nullptr` means the clipboard was cleared.
        DestroyOffer(g_selectionOffer);
        g_selectionOffer = offer;
    }

    constexpr wl_data_device_listener kDeviceListener = {
        .data_offer = OnDeviceDataOffer,
        .enter      = OnDeviceEnter,
        .leave      = OnDeviceLeave,
        .motion     = OnDeviceMotion,
        .drop       = OnDeviceDrop,
        .selection  = OnDeviceSelection,
    };

    const char* PickTextMime(const OfferState& s) {
        // Same priority order we advertise from SetText, so a round-trip
        // through ourselves picks the richest mime first.
        constexpr std::array kWantedMimes = {
            "text/plain;charset=utf-8",
            "text/plain",
            "UTF8_STRING",
            "TEXT",
        };
        for (const char* want : kWantedMimes) {
            for (const auto& m : s.mimes) {
                if (m == want) return want;
            }
        }
        return nullptr;
    }
}
#endif

#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
// Single JS import — the env.js implementation calls navigator.clipboard.writeText.
// `bool` return mirrors the native paths so callers don't need to #ifdef the
// check; failure cases (no permission, async rejection) just log to console.
// Lives in a named namespace (anonymous would give internal linkage and the
// import_module attribute would be dropped at link time).
namespace Crafter::DomEnv {
    __attribute__((import_module("env"), import_name("clipboardSetText")))
    bool DomClipboardSetText(const char* text, std::int32_t len);

    // JS returns either 0 (no text latched) or a wasm-malloc'd NUL-
    // terminated UTF-8 pointer that we own and free. Matches the
    // existing `getValue` ABI used by HtmlElementPtr::GetValue.
    __attribute__((import_module("env"), import_name("clipboardGetText")))
    const char* DomClipboardGetText();
}
#endif

void Crafter::Clipboard::Initialize() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    if (g_listenerAttached) return;
    if (Device::dataDevice == nullptr) return;   // compositor doesn't expose it
    wl_data_device_add_listener(Device::dataDevice, &kDeviceListener, nullptr);
    g_listenerAttached = true;
#endif
}

bool Crafter::Clipboard::SetText(std::string_view text) {
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
    return Crafter::DomEnv::DomClipboardSetText(text.data(), static_cast<std::int32_t>(text.size()));
#elif defined(CRAFTER_GRAPHICS_WINDOW_WAYLAND)
    if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) {
        // Compositor doesn't expose wl_data_device_manager (rare; some
        // headless / minimal compositors). Caller can fall back.
        return false;
    }
    // wl_data_device.set_selection requires a serial from a recent
    // input event. We track the most recent pointer-enter serial on
    // each window — fine for "user clicked Copy" interactions, since
    // the click itself produced the serial. Without one, the
    // compositor would silently reject the request.
    std::uint32_t serial = 0;
    if (Device::focusedWindow != nullptr) {
        serial = Device::focusedWindow->lastPointerSerial_;
    }
    if (serial == 0) return false;

    auto* held = new Held{ std::string(text) };
    wl_data_source* source =
        wl_data_device_manager_create_data_source(Device::dataDeviceManager);
    if (source == nullptr) {
        delete held;
        return false;
    }
    wl_data_source_add_listener(source, &kSourceListener, held);

    // Advertise the four common text MIME types so legacy + modern
    // pasters both find a match. Wayland clients prefer the first one
    // they recognise, in order.
    wl_data_source_offer(source, "text/plain;charset=utf-8");
    wl_data_source_offer(source, "text/plain");
    wl_data_source_offer(source, "UTF8_STRING");
    wl_data_source_offer(source, "TEXT");

    wl_data_device_set_selection(Device::dataDevice, source, serial);
    // Push the request so the compositor sees it before the next event
    // loop iteration — otherwise a quick read in another app might
    // miss the selection update.
    wl_display_flush(Device::display);
    return true;
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
    // CF_UNICODETEXT round-trip. Convert UTF-8 → UTF-16, allocate a
    // moveable HGLOBAL, hand it off to the OS. The OS frees the global
    // when the next clipboard owner replaces the data, so our caller
    // doesn't need to keep `text` alive — same lifetime contract as
    // the Wayland path.
    if (!OpenClipboard(nullptr)) return false;
    EmptyClipboard();
    const int wlen = MultiByteToWideChar(CP_UTF8, 0,
        text.data(), static_cast<int>(text.size()), nullptr, 0);
    if (wlen < 0) { CloseClipboard(); return false; }
    HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE,
        (static_cast<std::size_t>(wlen) + 1) * sizeof(wchar_t));
    if (h == nullptr) { CloseClipboard(); return false; }
    wchar_t* dst = static_cast<wchar_t*>(GlobalLock(h));
    if (dst == nullptr) {
        GlobalFree(h);
        CloseClipboard();
        return false;
    }
    MultiByteToWideChar(CP_UTF8, 0,
        text.data(), static_cast<int>(text.size()), dst, wlen);
    dst[wlen] = L'\0';
    GlobalUnlock(h);
    const bool ok = SetClipboardData(CF_UNICODETEXT, h) != nullptr;
    if (!ok) GlobalFree(h);     // OS only takes ownership on success
    CloseClipboard();
    return ok;
#else
    (void)text;
    return false;
#endif
}

std::optional<std::string> Crafter::Clipboard::GetText() {
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
    const char* raw = Crafter::DomEnv::DomClipboardGetText();
    if (raw == nullptr) return std::nullopt;
    std::string out(raw);
    std::free(const_cast<char*>(raw));   // JS side allocated via WasmAlloc/malloc
    return out;
#elif defined(CRAFTER_GRAPHICS_WINDOW_WAYLAND)
    if (g_selectionOffer == nullptr) return std::nullopt;
    auto it = g_offers.find(g_selectionOffer);
    if (it == g_offers.end()) return std::nullopt;
    const char* mime = PickTextMime(it->second);
    if (mime == nullptr) return std::nullopt;   // selection holds no text mime

    int fds[2];
    if (::pipe(fds) != 0) return std::nullopt;
    // Read end stays blocking; we drive it with poll() below.

    wl_data_offer_receive(g_selectionOffer, mime, fds[1]);
    // Flush so the compositor forwards `receive` to the source app.
    wl_display_flush(Device::display);
    // Drop our copy of the write end. The source app's copy stays open
    // until it finishes writing; once it closes, our read sees EOF.
    ::close(fds[1]);

    std::string out;
    const int wlFd = wl_display_get_fd(Device::display);

    // One-second budget. The poll loop also drives the Wayland event
    // pump so that if *we* are the source of the selection, our own
    // data_source.send callback can run and write into the pipe — a
    // plain blocking read on the pipe would deadlock in that case.
    constexpr int kTimeoutMs = 1000;
    auto deadline = std::chrono::steady_clock::now()
                    + std::chrono::milliseconds(kTimeoutMs);

    bool done = false;
    while (!done) {
        // Prepare for a non-blocking read on the wayland fd. The
        // prepare/read pair lets us safely interleave with poll().
        while (wl_display_prepare_read(Device::display) != 0) {
            wl_display_dispatch_pending(Device::display);
        }
        wl_display_flush(Device::display);

        const auto now = std::chrono::steady_clock::now();
        int remaining = static_cast<int>(
            std::chrono::duration_cast<std::chrono::milliseconds>(
                deadline - now).count());
        if (remaining < 0) remaining = 0;

        pollfd pfds[2] = {
            { fds[0], POLLIN, 0 },
            { wlFd,   POLLIN, 0 },
        };
        const int n = ::poll(pfds, 2, remaining);
        if (n < 0) {
            wl_display_cancel_read(Device::display);
            if (errno == EINTR) continue;
            break;
        }
        if (n == 0) {
            wl_display_cancel_read(Device::display);
            break;     // timeout
        }

        if (pfds[1].revents & POLLIN) {
            wl_display_read_events(Device::display);
            wl_display_dispatch_pending(Device::display);
        } else {
            wl_display_cancel_read(Device::display);
        }

        if (pfds[0].revents & (POLLIN | POLLHUP)) {
            char buf[4096];
            while (true) {
                const ssize_t r = ::read(fds[0], buf, sizeof(buf));
                if (r > 0) {
                    out.append(buf, static_cast<std::size_t>(r));
                    continue;
                }
                if (r == 0) { done = true; break; }   // EOF
                if (errno == EINTR) continue;
                if (errno == EAGAIN) break;           // shouldn't happen (blocking), but harmless
                done = true;
                break;
            }
        }
        if (pfds[0].revents & POLLERR) {
            break;
        }
    }
    ::close(fds[0]);
    if (out.empty()) return std::nullopt;
    return out;
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
    // OS owns the wide-char buffer until we CloseClipboard, so the
    // copy-out into std::string must happen before we release the lock.
    if (!OpenClipboard(nullptr)) return std::nullopt;
    HANDLE h = GetClipboardData(CF_UNICODETEXT);
    if (h == nullptr) { CloseClipboard(); return std::nullopt; }
    const wchar_t* src = static_cast<const wchar_t*>(GlobalLock(h));
    if (src == nullptr) { CloseClipboard(); return std::nullopt; }

    // CF_UNICODETEXT is NUL-terminated; lstrlenW gives us the length
    // without the terminator. WideCharToMultiByte with length excluding
    // the terminator produces a length-prefixed UTF-8 buffer we can
    // size std::string for exactly.
    const int wlen = lstrlenW(src);
    std::string out;
    if (wlen > 0) {
        const int u8len = WideCharToMultiByte(CP_UTF8, 0, src, wlen,
            nullptr, 0, nullptr, nullptr);
        if (u8len > 0) {
            out.resize(static_cast<std::size_t>(u8len));
            WideCharToMultiByte(CP_UTF8, 0, src, wlen,
                out.data(), u8len, nullptr, nullptr);
        }
    }
    GlobalUnlock(h);
    CloseClipboard();
    if (out.empty()) return std::nullopt;
    return out;
#else
    return std::nullopt;
#endif
}
