// drag_out_win.cpp — see drag_out_win.h. Hand-rolled IDataObject/IDropSource rather
// than a helper library: the object is tiny (one format, one medium) and the
// copy-only guarantee must be structural and auditable in one place. No REAPER API
// used here (pure OS/COM).

#include "shell/actions/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;

    // Convert each UTF-8 path to wide, normalizing '/' -> '\\' (the panel stores paths
    // slash-normalized; 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);
        if (!w.empty() && w.back() == L'\0') w.pop_back();  // re-added below
        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;
}

// COM reference counts MUST be interlocked. A CF_HDROP target is free to marshal the data
// object into another apartment and finish the copy on a background thread AFTER DoDragDrop
// has returned; Explorer's async file copy is suspected to do exactly this, though that
// specific behavior is not confirmed by experiment. A plain ++/-- there races the source
// thread's post-DoDragDrop Release: one lost increment destroys the object — and with it the
// source HGLOBAL — before the target reads it, and the drop lands with no file. That race is
// intermittent and a retry usually wins it; do not "simplify" these back.
inline ULONG comAddRef(volatile LONG& refs) {
    return static_cast<ULONG>(InterlockedIncrement(&refs));
}

// DoDragDrop requires the calling thread to be OLE-initialized — CoInitialize alone is not
// enough, and an uninitialized thread fails the call outright, so the drag never starts.
// Relying on REAPER having done it is a first-use hazard: whether it has depends on what else
// ran first in the session. OleInitialize is per-thread refcounted, so this is additive to
// whatever the host did; we deliberately never OleUninitialize — the extension lives for the
// process, and unbalancing a REAPER-owned apartment is the hazard worth avoiding, not this.
// RPC_E_CHANGED_MODE means the thread joined an MTA, where OLE drag-drop is unavailable.
bool ensureOleForThisThread() {
    static thread_local int state = 0;  // 0 untried, 1 ready, -1 unavailable
    if (state == 0) state = SUCCEEDED(OleInitialize(nullptr)) ? 1 : -1;
    return state > 0;
}

// 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 comAddRef(refs_); }
    ULONG STDMETHODCALLTYPE Release() override {
        const LONG r = InterlockedDecrement(&refs_);
        if (r == 0) delete this;
        return static_cast<ULONG>(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:
    volatile LONG 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 comAddRef(refs_); }
    ULONG STDMETHODCALLTYPE Release() override {
        const LONG r = InterlockedDecrement(&refs_);
        if (r == 0) delete this;
        return static_cast<ULONG>(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;
    }
    volatile LONG refs_ = 1;
    HGLOBAL hdrop_ = nullptr;
};

}  // namespace

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

    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: the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is never
    // offered, so no drop target can relocate (delete) the bank file.
    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;
}

bool canInitiateDragOut(const std::vector<std::string>& absolutePaths) {
    if (absolutePaths.empty()) return false;
    if (!ensureOleForThisThread()) return false;
    HGLOBAL hdrop = buildHDrop(absolutePaths);
    if (!hdrop) return false;
    GlobalFree(hdrop);  // probe only — initiateDragOut builds its own on the real attempt
    return true;
}

}  // namespace reasampler

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

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

namespace reasampler {

// SWELL_InitiateDragDropOfFileList initiates a copy-style file drag from the given
// window. Unlike OLE it exposes no per-source effect mask, so the copy-only
// guarantee here rests on SWELL's copy semantics rather than an explicit mask.
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)
}

// SWELL exposes no readiness probe ahead of SWELL_InitiateDragDropOfFileList (which itself
// reports no accept/cancel outcome) — the non-empty check is the only thing knowable in
// advance on this platform.
bool canInitiateDragOut(const std::vector<std::string>& absolutePaths) {
    return !absolutePaths.empty();
}

}  // namespace reasampler

#endif
