// drag_out_win — OS/COM initiation of native OS drag-out (M11). See drag_out_win.h.
//
// Windows path (primary): a hand-rolled minimal IDataObject exposing exactly one format,
// CF_HDROP, plus a minimal IDropSource, handed to OLE DoDragDrop with a COPY-ONLY effect
// mask. We roll our own rather than pull in a helper because the object is tiny (one
// format, one medium) and the copy-only guarantee must be structural and auditable in one
// place. mac/linux route to SWELL's file-list drag behind the same seam.
//
// Compiled into the reaper_reasampler MODULE. No REAPER API is used here (pure OS/COM); it
// is a leaf the bank_panel calls.

#include "drag_out_win.h"

#ifdef _WIN32

#include <windows.h>
#include <ole2.h>       // DoDragDrop, IDataObject, IDropSource, ReleaseStgMedium
#include <shlobj.h>     // DROPFILES, CF_HDROP
#include <cstring>

namespace reasampler {

namespace {

// Builds the CF_HDROP HGLOBAL: a DROPFILES header followed by a double-null-terminated
// list of wide (UTF-16) absolute paths. Windows CF_HDROP requires backslash separators and
// a trailing extra NUL after the final path's NUL. Returns nullptr on allocation failure or
// empty input. Ownership transfers to the STGMEDIUM (freed by ReleaseStgMedium / the OS).
HGLOBAL buildHDrop(const std::vector<std::string>& paths) {
    if (paths.empty()) return nullptr;

    // 1) Convert each UTF-8 path to wide, normalizing '/' -> '\\' (the panel stores paths
    //    slash-normalized for its own resolution; CF_HDROP wants native backslashes).
    std::vector<std::wstring> wide;
    wide.reserve(paths.size());
    std::size_t totalChars = 0;  // characters incl. each path's terminating NUL
    for (const std::string& p : paths) {
        if (p.empty()) continue;
        const int need = MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, nullptr, 0);
        if (need <= 0) continue;  // unconvertible path — skip rather than emit garbage
        std::wstring w(static_cast<std::size_t>(need), L'\0');
        MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, &w[0], need);
        // `need` includes the NUL; drop it from the string length, we re-add it in the buffer.
        if (!w.empty() && w.back() == L'\0') w.pop_back();
        for (wchar_t& c : w) if (c == L'/') c = L'\\';
        totalChars += w.size() + 1;  // + the per-path NUL
        wide.push_back(std::move(w));
    }
    if (wide.empty()) return nullptr;
    totalChars += 1;  // the extra double-NUL terminator after the last path

    const SIZE_T bytes = sizeof(DROPFILES) + totalChars * sizeof(wchar_t);
    HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes);
    if (!h) return nullptr;

    auto* df = static_cast<DROPFILES*>(GlobalLock(h));
    if (!df) { GlobalFree(h); return nullptr; }
    df->pFiles = sizeof(DROPFILES);  // offset to the file list
    df->fWide  = TRUE;               // wide (UTF-16) path list

    auto* dst = reinterpret_cast<wchar_t*>(reinterpret_cast<char*>(df) + sizeof(DROPFILES));
    for (const std::wstring& w : wide) {
        std::memcpy(dst, w.c_str(), (w.size() + 1) * sizeof(wchar_t));  // incl. NUL
        dst += w.size() + 1;
    }
    *dst = L'\0';  // double-NUL terminates the list (ZEROINIT already did, explicit for clarity)

    GlobalUnlock(h);
    return h;
}

// Minimal IDropSource: continue until the (left) button releases or Escape cancels; always
// request the copy cursor. This is the standard textbook drop source — no custom feedback.
class DropSource final : public IDropSource {
public:
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
        if (riid == IID_IUnknown || riid == IID_IDropSource) {
            *ppv = static_cast<IDropSource*>(this);
            AddRef();
            return S_OK;
        }
        *ppv = nullptr;
        return E_NOINTERFACE;
    }
    ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
    ULONG STDMETHODCALLTYPE Release() override {
        const ULONG r = --refs_;
        if (r == 0) delete this;
        return r;
    }
    // IDropSource
    HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
        if (escapePressed) return DRAGDROP_S_CANCEL;
        if (!(keyState & MK_LBUTTON)) return DRAGDROP_S_DROP;  // released -> drop
        return S_OK;                                            // keep dragging
    }
    HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD /*effect*/) override {
        return DRAGDROP_S_USEDEFAULTCURSORS;  // let OLE draw the standard copy cursor
    }
private:
    ULONG refs_ = 1;
};

// Minimal IDataObject exposing exactly one format (CF_HDROP / TYMED_HGLOBAL). The HDROP is
// built once at construction and cloned on each GetData call (OLE owns the returned medium).
class HDropDataObject final : public IDataObject {
public:
    explicit HDropDataObject(HGLOBAL hdrop) : hdrop_(hdrop) {}
    ~HDropDataObject() { if (hdrop_) GlobalFree(hdrop_); }

    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
        if (riid == IID_IUnknown || riid == IID_IDataObject) {
            *ppv = static_cast<IDataObject*>(this);
            AddRef();
            return S_OK;
        }
        *ppv = nullptr;
        return E_NOINTERFACE;
    }
    ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
    ULONG STDMETHODCALLTYPE Release() override {
        const ULONG r = --refs_;
        if (r == 0) delete this;
        return r;
    }

    // IDataObject — the two that matter for a drag source.
    HRESULT STDMETHODCALLTYPE GetData(FORMATETC* fmt, STGMEDIUM* med) override {
        if (!fmt || !med) return E_INVALIDARG;
        if (!isHDrop(*fmt)) return DV_E_FORMATETC;
        if (!hdrop_) return E_UNEXPECTED;

        // Clone the HGLOBAL so the caller (OLE / target) owns an independent copy; our
        // hdrop_ stays valid for repeat GetData calls and is freed in the dtor.
        const SIZE_T sz = GlobalSize(hdrop_);
        HGLOBAL copy = GlobalAlloc(GMEM_MOVEABLE, sz);
        if (!copy) return E_OUTOFMEMORY;
        void* src = GlobalLock(hdrop_);
        if (!src) { GlobalFree(copy); return E_OUTOFMEMORY; }
        void* dst = GlobalLock(copy);
        if (!dst) { GlobalUnlock(hdrop_); GlobalFree(copy); return E_OUTOFMEMORY; }
        std::memcpy(dst, src, sz);
        GlobalUnlock(hdrop_);
        GlobalUnlock(copy);

        med->tymed = TYMED_HGLOBAL;
        med->hGlobal = copy;
        med->pUnkForRelease = nullptr;  // caller releases via ReleaseStgMedium
        return S_OK;
    }
    HRESULT STDMETHODCALLTYPE QueryGetData(FORMATETC* fmt) override {
        return (fmt && isHDrop(*fmt)) ? S_OK : DV_E_FORMATETC;
    }

    // The remainder are the standard "not supported for a simple source" stubs.
    HRESULT STDMETHODCALLTYPE GetDataHere(FORMATETC*, STGMEDIUM*) override { return E_NOTIMPL; }
    HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(FORMATETC*, FORMATETC* out) override {
        if (out) out->ptd = nullptr;
        return E_NOTIMPL;
    }
    HRESULT STDMETHODCALLTYPE SetData(FORMATETC*, STGMEDIUM*, BOOL) override { return E_NOTIMPL; }
    HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dir, IEnumFORMATETC** out) override {
        if (dir == DATADIR_GET && out) {
            FORMATETC fe = hdropFormat();
            return SHCreateStdEnumFmtEtc(1, &fe, out);
        }
        return E_NOTIMPL;
    }
    HRESULT STDMETHODCALLTYPE DAdvise(FORMATETC*, DWORD, IAdviseSink*, DWORD*) override {
        return OLE_E_ADVISENOTSUPPORTED;
    }
    HRESULT STDMETHODCALLTYPE DUnadvise(DWORD) override { return OLE_E_ADVISENOTSUPPORTED; }
    HRESULT STDMETHODCALLTYPE EnumDAdvise(IEnumSTATDATA**) override {
        return OLE_E_ADVISENOTSUPPORTED;
    }

private:
    static FORMATETC hdropFormat() {
        FORMATETC fe{};
        fe.cfFormat = CF_HDROP;
        fe.ptd = nullptr;
        fe.dwAspect = DVASPECT_CONTENT;
        fe.lindex = -1;
        fe.tymed = TYMED_HGLOBAL;
        return fe;
    }
    static bool isHDrop(const FORMATETC& fe) {
        return fe.cfFormat == CF_HDROP &&
               (fe.tymed & TYMED_HGLOBAL) &&
               fe.dwAspect == DVASPECT_CONTENT;
    }
    ULONG refs_ = 1;
    HGLOBAL hdrop_ = nullptr;
};

}  // namespace

bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths) {
    if (absolutePaths.empty()) return false;

    // REAPER's main thread is already OLE-initialized (it hosts OLE drag targets), so we do
    // NOT call OleInitialize here — a nested OleInitialize on an already-initialized STA is
    // harmless-but-unnecessary, and OleUninitialize pairing across a REAPER-owned apartment
    // is the kind of thing that bites. DoDragDrop works on the already-initialized STA.
    HGLOBAL hdrop = buildHDrop(absolutePaths);
    if (!hdrop) return false;

    auto* data = new HDropDataObject(hdrop);  // takes ownership of hdrop
    auto* source = new DropSource();

    DWORD effect = 0;
    // COPY-ONLY (invariant #1): the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is
    // NEVER offered, so no drop target can relocate (delete) the bank file — only prune
    // deletes bank bytes (Phase R boundary).
    const HRESULT hr = DoDragDrop(data, source, DROPEFFECT_COPY, &effect);

    source->Release();
    data->Release();  // frees the source HGLOBAL via HDropDataObject's dtor

    return hr == DRAGDROP_S_DROP && effect == DROPEFFECT_COPY;
}

}  // namespace reasampler

#else  // ---- macOS / Linux (SWELL) -----------------------------------------------------

#include "wdltypes.h"
#include "swell/swell.h"
#include <vector>

namespace reasampler {

// SWELL provides a file-list drag surface (SWELL_InitiateDragDropOfFileList, verified in
// vendor/WDL/WDL/swell/swell-functions.h). It takes a C-string array + count and initiates
// a copy-style file drag from the given window. Unlike OLE it exposes no per-source effect
// mask, so the copy-only guarantee rests on SWELL's copy semantics rather than an explicit
// DROPEFFECT_COPY mask — an honest platform difference, not a faked equivalence. Windows is
// the exact-control path (D5: Windows is the shipping target).
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths) {
    if (absolutePaths.empty() || !panelHwnd) return false;

    std::vector<const char*> ptrs;
    ptrs.reserve(absolutePaths.size());
    for (const std::string& p : absolutePaths) ptrs.push_back(p.c_str());

    // srcrect null: SWELL positions the drag image at the current event. No custom icon.
    SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), nullptr,
                                     ptrs.data(), static_cast<int>(ptrs.size()), nullptr);
    return true;  // fire-and-forget; SWELL owns the drag from here (no accept/cancel return)
}

}  // namespace reasampler

#endif
