//! 系统级拖放(OLE):把面板里选中的东西拖给别的程序(含资源管理器)。
//!
//! **只做拖出**。拖入不走这里:winit 0.30 自己已经把主窗口注册成了 `IDropTarget`
//! (我们再去 `RegisterDragDrop` 会拿到 `DRAGDROP_E_ALREADYREGISTERED`),它把落点转成
//! `WindowEvent::DroppedFile`,egui 再暴露成 `raw.dropped_files` / `raw.hovered_files`。
//! 两套并存只会互相抢注册,所以拖入统一走 winit/egui 那条通道。
//!
//! 拖出:自建 `CF_HDROP` 数据对象 + `IDropSource`,交给 `DoDragDrop` 阻塞到放下或取消。
//! `DoDragDrop` 期间 OLE 自己泵消息,所以必须当作**阻塞模态**对待:置位 [`set_modal_active`],
//! 让窗口 proc 拦下 `WM_PAINT`(否则 winit 会重入 egui,eframe 会 panic)。

use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};

/// 拖拽最终生效的效果。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DragEffect {
    None,
    Copy,
    Move,
}

/// 阻塞式模态标记:原生菜单与拖出循环**共用**一份(见 [`set_modal_active`])。
static MODAL: AtomicBool = AtomicBool::new(false);

/// 把一组路径交给系统拖放;**阻塞**到放下或取消,必须在 UI 线程调用。
///
/// `allow_move = false` 时只允许 COPY("绝不动原件"的场合)。
pub fn drag_out(hwnd: isize, paths: &[PathBuf], allow_move: bool) -> Result<DragEffect, String> {
    platform::drag_out(hwnd, paths, allow_move)
}

/// 阻塞式模态(原生菜单 / 拖放循环)期间置位:窗口 proc 要拦下 `WM_PAINT`,避免重入 egui。
///
/// 原生菜单(`shell::menu`)与拖出循环都会置位、结束再清掉;两者不可能同时发生,
/// 所以共用这一份标记最简单。
pub fn set_modal_active(active: bool) {
    MODAL.store(active, Ordering::SeqCst);
}

pub fn is_modal_active() -> bool {
    // 窗口过程每条消息都会问一次,Relaxed 足够(只要"稍后可见")。
    MODAL.load(Ordering::Relaxed)
}

#[cfg(windows)]
mod platform {
    use std::cell::Cell;
    use std::ffi::c_void;
    use std::os::windows::ffi::OsStrExt;
    use std::path::PathBuf;

    use windows::core::{implement, HRESULT, BOOL, PCWSTR};
    use windows::Win32::Foundation::{
        GlobalFree, DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, DRAGDROP_S_USEDEFAULTCURSORS,
        DV_E_DVASPECT, DV_E_FORMATETC, E_NOTIMPL, E_OUTOFMEMORY, E_POINTER, HGLOBAL, HWND,
        OLE_E_ADVISENOTSUPPORTED, OLE_E_NOCONNECTION, POINT, RPC_E_CHANGED_MODE, S_FALSE, S_OK,
    };
    use windows::Win32::System::Com::{
        IAdviseSink, IDataObject, IDataObject_Impl, IEnumFORMATETC, IEnumFORMATETC_Impl, IEnumSTATDATA,
        DATADIR_GET, DVASPECT_CONTENT,
        FORMATETC, STGMEDIUM, STGMEDIUM_0, TYMED_HGLOBAL,
    };
    use windows::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE};
    use windows::Win32::System::DataExchange::RegisterClipboardFormatW;
use windows::Win32::System::Ole::{
        DoDragDrop, IDropSource, IDropSource_Impl, OleInitialize, ReleaseStgMedium, CF_HDROP, DROPEFFECT,
        DROPEFFECT_COPY, DROPEFFECT_MOVE, DROPEFFECT_NONE,
    };
    use windows::Win32::System::SystemServices::{MK_LBUTTON, MK_RBUTTON, MODIFIERKEYS_FLAGS};
    use windows::Win32::UI::Shell::DROPFILES;
    use windows::Win32::UI::WindowsAndMessaging::IsWindow;

    use crate::flog;

    use super::DragEffect;

    /// `DROPFILES` 头 + UTF-16 路径 + 结尾双 `\0`:`CF_HDROP` 的标准载荷。
    ///
    /// 只该传绝对路径(相对路径在 shell 那边解出来是错的),调用方(UI)保证。
    fn pack_hdrop(paths: &[PathBuf]) -> Vec<u8> {
        let header = DROPFILES {
            pFiles: std::mem::size_of::<DROPFILES>() as u32,
            pt: POINT::default(),
            fNC: BOOL(0),
            fWide: BOOL(1),
        };
        let mut bytes = Vec::with_capacity(std::mem::size_of::<DROPFILES>() + 64);
        // SAFETY: DROPFILES 是 POD,按字节读它不会碰任何指针。
        bytes.extend_from_slice(unsafe { as_bytes(&header) });
        for path in paths {
            for unit in path.as_os_str().encode_wide() {
                bytes.extend_from_slice(&unit.to_ne_bytes());
            }
            bytes.extend_from_slice(&0u16.to_ne_bytes());
        }
        // 列表结束的第二个 `\0`(每一项各自以 `\0` 结尾,最后再补一个)。
        bytes.extend_from_slice(&0u16.to_ne_bytes());
        bytes
    }

    /// 把字节拷进一块 `GMEM_MOVEABLE` 全局内存,返回句柄。
    fn hglobal_from_bytes(bytes: &[u8]) -> Option<HGLOBAL> {
        if bytes.is_empty() {
            return None;
        }
        unsafe {
            let handle = GlobalAlloc(GMEM_MOVEABLE, bytes.len()).ok()?;
            let target = GlobalLock(handle);
            if target.is_null() {
                let _ = GlobalFree(Some(handle));
                return None;
            }
            std::ptr::copy_nonoverlapping(bytes.as_ptr(), target as *mut u8, bytes.len());
            // 交给别人之前解锁;Win32 里句柄值就是可用的指针(HDROP 即此值)。
            let _ = GlobalUnlock(handle);
            Some(handle)
        }
    }

    /// `"Preferred DropEffect"` 的会话内格式号:源端用它声明"我允许/偏好哪种效果"。
    fn preferred_drop_effect() -> u16 {
        static ID: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
        *ID.get_or_init(|| register_format("Preferred DropEffect"))
    }

    /// `"Performed DropEffect"` 的会话内格式号:落点用它回报"我实际做了什么"。
    fn performed_drop_effect() -> u16 {
        static ID: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
        *ID.get_or_init(|| register_format("Performed DropEffect"))
    }

    fn register_format(name: &str) -> u16 {
        let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
        // SAFETY: 传的是以 0 结尾的宽字符串。
        let id = unsafe { RegisterClipboardFormatW(PCWSTR(wide.as_ptr())) };
        (id & 0xffff) as u16
    }

    /// `CFSTR_PREFERREDDROPEFFECT` 那一项(DWORD / 全局内存 / 内容视图)。
    fn is_preferred_effect_format(format: &FORMATETC) -> bool {
        format.cfFormat == preferred_drop_effect()
            && format.tymed & (TYMED_HGLOBAL.0 as u32) != 0
            && format.dwAspect == DVASPECT_CONTENT.0
    }

    /// 这个 `FORMATETC` 是不是我们要的那份(`CF_HDROP` / 全局内存 / 内容视图)。
    fn is_hdrop_format(format: &FORMATETC) -> bool {
        format.cfFormat == CF_HDROP.0
            && format.tymed & (TYMED_HGLOBAL.0 as u32) != 0
            && format.dwAspect == DVASPECT_CONTENT.0
    }

    // ---------------------------------------------------------------- 拖出的两个 COM 对象

    /// 提供 `CF_HDROP` + `CFSTR_PREFERREDDROPEFFECT` 两项的数据对象。
    ///
    /// 只给 `CF_HDROP` 在自家窗口里够用(winit 的放置目标只读 `CF_HDROP`),
    /// 但系统资源管理器这类"富"落点会额外索取偏好效果,并会把实际做的效果回写进
    /// `CFSTR_PERFORMEDDROPEFFECT`——缺这两样它可能压根不给效果。
    #[implement(IDataObject)]
    struct DataObject {
        /// 打包好的 `DROPFILES` 载荷;每次 `GetData` 都拷一份新的全局内存出去。
        payload: Vec<u8>,
        /// 声明给落点的偏好效果(`DROPEFFECT_COPY` / `DROPEFFECT_MOVE` 的 DWORD)。
        preferred_effect: u32,
    }

    /// 数据对象只提供一项格式:`CF_HDROP`。
    ///
    /// 这个枚举器不是可有可无的:资源管理器在 `DragEnter`/`Drop` 前后会枚举数据对象的格式
    /// (它要顺带找 `CFSTR_FILEDESCRIPTOR` / `CFSTR_PREFERREDDROPEFFECT` 之类),
    /// 之前这里返回 `E_NOTIMPL`,它拿不到枚举就判定这份数据不能用,落点直接被拒
    /// ——实测现象就是拖到资源管理器 `DoDragDrop` 报 `DROPEFFECT_NONE`、文件纹丝不动。
    #[implement(IEnumFORMATETC)]
    struct FormatEtcEnum {
        /// `#[implement]` 会给 `_Impl` 生成同名成员,这里叫 `total` 避让。
        total: usize,
        index: std::sync::atomic::AtomicUsize,
    }

    impl FormatEtcEnum {
        fn new() -> Self {
            Self {
                total: 2,
                index: std::sync::atomic::AtomicUsize::new(0),
            }
        }

        /// 第 `i` 项格式:0 = `CF_HDROP`,1 = 偏好效果。
        fn format_at(i: usize) -> FORMATETC {
            FORMATETC {
                cfFormat: if i == 0 { CF_HDROP.0 } else { preferred_drop_effect() },
                ptd: std::ptr::null_mut(),
                dwAspect: DVASPECT_CONTENT.0,
                lindex: -1,
                tymed: TYMED_HGLOBAL.0 as u32,
            }
        }
    }

    impl IEnumFORMATETC_Impl for FormatEtcEnum_Impl {
        fn Next(&self, celt: u32, rgelt: *mut FORMATETC, fetched: *mut u32) -> HRESULT {
            use std::sync::atomic::Ordering::Relaxed;
            let mut got = 0u32;
            let mut i = self.index.load(Relaxed);
            while got < celt && i < self.total {
                if !rgelt.is_null() {
                    // SAFETY: 调用方按 celt 准备了空间。
                    unsafe { *rgelt.add(got as usize) = FormatEtcEnum::format_at(i) };
                }
                got += 1;
                i += 1;
            }
            self.index.store(i, Relaxed);
            if !fetched.is_null() {
                // SAFETY: 调用方给了可写指针。
                unsafe { *fetched = got };
            }
            // 按规范:一项没给出来就是 S_FALSE。
            if got == celt { S_OK } else { S_FALSE }
        }

        fn Skip(&self, celt: u32) -> windows::core::Result<()> {
            use std::sync::atomic::Ordering::Relaxed;
            let target = self.index.load(Relaxed).saturating_add(celt as usize);
            self.index.store(target, Relaxed);
            // windows-rs 把 Skip 映射成 Result,S_FALSE("没跳满")只能从 Err 走回 ABI。
            if target <= self.total {
                Ok(())
            } else {
                Err(S_FALSE.into())
            }
        }

        fn Reset(&self) -> windows::core::Result<()> {
            self.index.store(0, std::sync::atomic::Ordering::Relaxed);
            Ok(())
        }

        fn Clone(&self) -> windows::core::Result<IEnumFORMATETC> {
            use std::sync::atomic::Ordering::Relaxed;
            let clone = FormatEtcEnum::new();
            clone.index.store(self.index.load(Relaxed), Relaxed);
            Ok(clone.into())
        }
    }

    impl IDataObject_Impl for DataObject_Impl {
        fn GetData(&self, format: *const FORMATETC) -> windows::core::Result<STGMEDIUM> {
            if format.is_null() {
                return Err(E_POINTER.into());
            }
            // SAFETY: 上面判了非空,OLE 保证指针有效。
            let format = unsafe { &*format };
            if format.dwAspect != DVASPECT_CONTENT.0 {
                return Err(DV_E_DVASPECT.into());
            }
            if is_preferred_effect_format(format) {
                let bytes = self.preferred_effect.to_ne_bytes();
                let Some(handle) = hglobal_from_bytes(&bytes) else {
                    return Err(E_OUTOFMEMORY.into());
                };
                return Ok(STGMEDIUM {
                    tymed: TYMED_HGLOBAL.0 as u32,
                    u: STGMEDIUM_0 { hGlobal: handle },
                    pUnkForRelease: std::mem::ManuallyDrop::new(None),
                });
            }
            if format.cfFormat != CF_HDROP.0 || format.tymed & (TYMED_HGLOBAL.0 as u32) == 0 {
                return Err(DV_E_FORMATETC.into());
            }
            let Some(handle) = hglobal_from_bytes(&self.payload) else {
                flog!("[shell] 拖拽:把 CF_HDROP 载荷拷进全局内存失败");
                return Err(E_OUTOFMEMORY.into());
            };
            Ok(STGMEDIUM {
                tymed: TYMED_HGLOBAL.0 as u32,
                u: STGMEDIUM_0 { hGlobal: handle },
                // 没人代为释放:拿到的人自己 `ReleaseStgMedium`。
                pUnkForRelease: std::mem::ManuallyDrop::new(None),
            })
        }

        fn GetDataHere(
            &self,
            _format: *const FORMATETC,
            _medium: *mut STGMEDIUM,
        ) -> windows::core::Result<()> {
            // 拖放用不到:目标要数据都用 GetData 拿一份新的。
            Err(E_NOTIMPL.into())
        }

        fn QueryGetData(&self, format: *const FORMATETC) -> HRESULT {
            if format.is_null() {
                return E_POINTER;
            }
            // SAFETY: 上面判了非空。
            let f = unsafe { &*format };
            if is_hdrop_format(f) || is_preferred_effect_format(f) {
                S_OK
            } else {
                // 按规范:给不出这个格式时返回 S_FALSE,而不是错误码。
                S_FALSE
            }
        }

        fn GetCanonicalFormatEtc(
            &self,
            _format_in: *const FORMATETC,
            _format_out: *mut FORMATETC,
        ) -> HRESULT {
            E_NOTIMPL
        }

        fn SetData(
            &self,
            _format: *const FORMATETC,
            _medium: *const STGMEDIUM,
            _release: BOOL,
        ) -> windows::core::Result<()> {
            if _format.is_null() || _medium.is_null() {
                return Err(E_POINTER.into());
            }
            // SAFETY: 上面判了非空。
            let (format, medium) = unsafe { (&*_format, &*_medium) };
            if format.cfFormat != performed_drop_effect() {
                return Err(E_NOTIMPL.into());
            }
            // 落点回报"我实际做了 MOVE/COPY":记一行日志,并按规范接管这份介质。
            let value = if medium.tymed == TYMED_HGLOBAL.0 as u32 {
                // SAFETY: tymed 说了是全局内存,那就读这个联合成员。
                let handle = unsafe { medium.u.hGlobal };
                let ptr = unsafe { GlobalLock(handle) };
                if ptr.is_null() {
                    0
                } else {
                    // SAFETY: 这是落点写进来的 DWORD。
                    let value = unsafe { *(ptr as *const u32) };
                    let _ = unsafe { GlobalUnlock(handle) };
                    value
                }
            } else {
                0
            };
            crate::flog!("[drag]   落点回报所做的效果 = {value}");
            if _release.as_bool() {
                // 调用方连所有权一起给了:读完就还回去,别漏。
                unsafe { ReleaseStgMedium(_medium as *mut STGMEDIUM) };
            }
            Ok(())
        }

        fn EnumFormatEtc(&self, direction: u32) -> windows::core::Result<IEnumFORMATETC> {
            // 拖放只走"取"方向。
            if direction != DATADIR_GET.0 as u32 {
                return Err(E_NOTIMPL.into());
            }
            Ok(FormatEtcEnum::new().into())
        }

        fn DAdvise(
            &self,
            _format: *const FORMATETC,
            _flags: u32,
            _sink: windows::core::Ref<'_, IAdviseSink>,
        ) -> windows::core::Result<u32> {
            Err(OLE_E_ADVISENOTSUPPORTED.into())
        }

        fn DUnadvise(&self, _connection: u32) -> windows::core::Result<()> {
            Err(OLE_E_NOCONNECTION.into())
        }

        fn EnumDAdvise(&self) -> windows::core::Result<IEnumSTATDATA> {
            Err(OLE_E_ADVISENOTSUPPORTED.into())
        }
    }

    /// 拖出时的源行为:左键/右键松开都算「放下」,Esc 取消,其余交系统画默认光标。
    #[implement(IDropSource)]
    struct DropSource {}

    impl IDropSource_Impl for DropSource_Impl {
        fn QueryContinueDrag(&self, f_escape_pressed: BOOL, keystate: MODIFIERKEYS_FLAGS) -> HRESULT {
            if f_escape_pressed.as_bool() {
                return DRAGDROP_S_CANCEL;
            }
            // 两个键都松了才算放下:DoDragDrop 循环可能跑到别的应用(资源管理器)里,
            // 期间键状态由系统喂过来。
            if (keystate & MK_LBUTTON).0 == 0 && (keystate & MK_RBUTTON).0 == 0 {
                DRAGDROP_S_DROP
            } else {
                S_OK
            }
        }

        fn GiveFeedback(&self, _effect: DROPEFFECT) -> HRESULT {
            DRAGDROP_S_USEDEFAULTCURSORS
        }
    }

    /// 阻塞模态期间置位共享标记;RAII 保证路怎么走都能清回来
    /// (清不掉的话窗口会永远不响应 `WM_PAINT`)。
    struct ModalGuard;

    impl ModalGuard {
        fn enter() -> Self {
            super::set_modal_active(true);
            Self
        }
    }

    impl Drop for ModalGuard {
        fn drop(&mut self) {
            super::set_modal_active(false);
        }
    }

    // ---------------------------------------------------------------- OLE 初始化

    thread_local! {
        /// 本线程的 OLE 状态:0 = 没试过,1 = 可用,2 = 不可用(线程在 MTA)。
        static OLE_STATE: Cell<u8> = const { Cell::new(0) };
    }

    /// 保证当前线程的 OLE 已初始化。
    ///
    /// **故意不配对 `OleUninitialize`**:拖放是长期能力,而且 winit 自己也可能初始化过 OLE
    /// (那时 `OleInitialize` 返回 `S_FALSE`),我们再反初始化就把别人的拆了。
    /// 这是个长期存活的 GUI 进程,OLE 常驻是正常做法。
    fn ensure_ole() -> Result<(), String> {
        OLE_STATE.with(|state| match state.get() {
            1 => Ok(()),
            2 => Err("当前线程在 MTA 里(RPC_E_CHANGED_MODE),OLE 拖放不可用".to_string()),
            _ => {
                // `S_OK`(这次是我们初始化的)与 `S_FALSE`(本线程早就初始化过)都算可用,
                // 所以官方绑定的 `Result` 就够。
                match unsafe { OleInitialize(None) } {
                    Ok(()) => {
                        state.set(1);
                        Ok(())
                    }
                    Err(err) if err.code() == RPC_E_CHANGED_MODE => {
                        state.set(2);
                        Err("当前线程在 MTA 里(RPC_E_CHANGED_MODE),OLE 拖放不可用".to_string())
                    }
                    Err(err) => {
                        flog!("[shell] OleInitialize 失败:{err}");
                        Err(format!("初始化 OLE 失败:{err}"))
                    }
                }
            }
        })
    }

    // ---------------------------------------------------------------- 入口

    pub(super) fn drag_out(
        hwnd: isize,
        paths: &[PathBuf],
        allow_move: bool,
    ) -> Result<DragEffect, String> {
        if paths.is_empty() {
            return Err("没有可拖拽的路径".to_string());
        }
        let window = HWND(hwnd as *mut c_void);
        // SAFETY: IsWindow 只读句柄。
        if !unsafe { IsWindow(Some(window)) }.as_bool() {
            return Err(format!("窗口句柄无效:0x{hwnd:x}"));
        }
        ensure_ole()?;

        // 数据对象与拖放源在 DoDragDrop 期间由 OLE 持有;接口值的 Drop 放掉我们这份引用。
        let preferred = if allow_move {
            DROPEFFECT_MOVE.0
        } else {
            DROPEFFECT_COPY.0
        };
        let data: IDataObject = DataObject {
            payload: pack_hdrop(paths),
            preferred_effect: preferred,
        }
        .into();
        let source: IDropSource = DropSource {}.into();
        let allowed = if allow_move {
            DROPEFFECT_COPY | DROPEFFECT_MOVE
        } else {
            DROPEFFECT_COPY
        };
        let mut effect = DROPEFFECT_NONE;

        // DoDragDrop 期间 OLE 自己泵消息:标成模态,让窗口 proc 拦下 WM_PAINT(否则 winit 重入 egui)。
        let _modal = ModalGuard::enter();
        // SAFETY: data/source 都是本线程的 COM 对象,window 刚校验过。
        let hr = unsafe { DoDragDrop(&data, &source, allowed, &mut effect) };

        if hr == DRAGDROP_S_DROP {
            Ok(drag_effect(effect))
        } else if hr == DRAGDROP_S_CANCEL {
            Ok(DragEffect::None)
        } else {
            flog!("[shell] DoDragDrop 失败:{hr:?}");
            Err(format!("系统拖放失败:{hr:?}"))
        }
    }

    fn drag_effect(effect: DROPEFFECT) -> DragEffect {
        if effect == DROPEFFECT_COPY {
            DragEffect::Copy
        } else if effect == DROPEFFECT_MOVE {
            DragEffect::Move
        } else {
            DragEffect::None
        }
    }

    // ---------------------------------------------------------------- 工具

    /// 把一个 POD 当字节切片读。
    ///
    /// # Safety
    /// `T` 必须没有无效位模式、也不含需要 Drop 的字段。
    unsafe fn as_bytes<T>(value: &T) -> &[u8] {
        unsafe { std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::<T>()) }
    }

    /// 测试用:拖入改成 winit 那条通道后,生产代码只**写** `CF_HDROP`([`pack_hdrop`])。
    /// 这里的读回(`DragQueryFileW` / `IDataObject::GetData`)是为了让测试拿系统 API
    /// 反过来校验我们造的载荷与介质,所以整块只编进测试目标。
    #[cfg(test)]
    mod tests {
        use std::os::windows::ffi::OsStringExt;
        use std::path::PathBuf;

        use windows::Win32::Foundation::GlobalFree;
        use windows::Win32::System::Com::{
            IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL,
        };
        use windows::Win32::System::Ole::{ReleaseStgMedium, CF_HDROP};
        use windows::Win32::UI::Shell::{DragQueryFileW, HDROP};

        use super::*;

        /// `cargo test` 并行跑,`shell::menu` 的说明是"碰 shell 的测试都先拿这把锁":
        /// 这里要调 shell32 的 `DragQueryFileW`,同样收进锁里。
        fn serial() -> std::sync::MutexGuard<'static, ()> {
            crate::shell::menu::SHELL_TEST_LOCK
                .lock()
                .unwrap_or_else(|poisoned| poisoned.into_inner())
        }

        /// `CF_HDROP` + `TYMED_HGLOBAL` + `DVASPECT_CONTENT` 的 `FORMATETC`。
        fn hdrop_format() -> FORMATETC {
            FORMATETC {
                cfFormat: CF_HDROP.0,
                ptd: std::ptr::null_mut(),
                dwAspect: DVASPECT_CONTENT.0,
                lindex: -1,
                tymed: TYMED_HGLOBAL.0 as u32,
            }
        }

        /// 用系统 `DragQueryFileW` 把 `CF_HDROP` 载荷解回路径列表。
        ///
        /// # Safety
        /// `hdrop` 必须指向一块有效的 `DROPFILES`(`fWide` 为真),且调用期间不被释放。
        unsafe fn unpack_hdrop(hdrop: HDROP) -> Vec<PathBuf> {
            unsafe {
                let count = DragQueryFileW(hdrop, u32::MAX, None);
                let mut paths = Vec::with_capacity(count as usize);
                for index in 0..count {
                    // 不含结尾 NUL 的长度。
                    let len = DragQueryFileW(hdrop, index, None);
                    if len == 0 {
                        continue;
                    }
                    let mut buffer = vec![0u16; len as usize + 1];
                    let written = DragQueryFileW(hdrop, index, Some(&mut buffer));
                    buffer.truncate(written as usize);
                    // 用 OsString 而不是 String:路径里可能有落单的代理项,别做有损转换。
                    paths.push(PathBuf::from(std::ffi::OsString::from_wide(&buffer)));
                }
                paths
            }
        }

        /// `TYMED_HGLOBAL` 的 `CF_HDROP` 介质 → 路径列表。
        ///
        /// # Safety
        /// `medium` 必须是有效的 `STGMEDIUM`,且里面那支确实是 `hGlobal`。
        unsafe fn hdrop_paths(medium: &STGMEDIUM) -> Vec<PathBuf> {
            if medium.tymed & (TYMED_HGLOBAL.0 as u32) == 0 {
                return Vec::new();
            }
            // SAFETY: tymed 说是 HGLOBAL,按联合体的规定读它。
            let handle = unsafe { medium.u.hGlobal };
            if handle.is_invalid() {
                return Vec::new();
            }
            // SAFETY: HGLOBAL 装的是 DROPFILES 块。
            unsafe { unpack_hdrop(HDROP(handle.0)) }
        }

        #[test]
        fn hdrop_pack_unpack_roundtrip() {
            let _guard = serial();
            let paths = vec![
                PathBuf::from(r"C:\Program Files\有 空格 的目录\文件 名.txt"),
                PathBuf::from(r"D:\开发\FolderDock\src\shell\drag.rs"),
                PathBuf::from(r"E:\带 空格\子 目录\b"),
            ];

            let handle = hglobal_from_bytes(&pack_hdrop(&paths)).expect("分配 HGLOBAL 失败");
            // 用系统的 DragQueryFileW 从同一块内存解回来。
            let got = unsafe { unpack_hdrop(HDROP(handle.0)) };
            let _ = unsafe { GlobalFree(Some(handle)) };

            assert_eq!(got, paths);
        }

        #[test]
        fn data_object_get_data_returns_hdrop_medium() {
            let _guard = serial();
            let paths = vec![
                PathBuf::from(r"C:\项目 一\a b\文件.txt"),
                PathBuf::from(r"C:\项目 二\two.dll"),
            ];

            let object: IDataObject = DataObject {
                payload: pack_hdrop(&paths),
                preferred_effect: DROPEFFECT_COPY.0,
            }.into();

            // 直接调自己实现的 IDataObject::GetData(CF_HDROP)。
            let mut medium = unsafe { object.GetData(&hdrop_format()) }.expect("GetData(CF_HDROP) 应当成功");
            assert_eq!(medium.tymed, TYMED_HGLOBAL.0 as u32);
            // SAFETY: tymed 是 HGLOBAL。
            assert!(!unsafe { medium.u.hGlobal }.is_invalid());
            assert!(medium.pUnkForRelease.is_none());

            // 返回的介质要能被系统 DragQueryFileW 解出同样的路径。
            let got = unsafe { hdrop_paths(&medium) };
            assert_eq!(got, paths);
            unsafe { ReleaseStgMedium(&mut medium) };

            // 非 CF_HDROP 的格式要拒绝。
            let wrong = FORMATETC {
                cfFormat: 1, // CF_TEXT
                ptd: std::ptr::null_mut(),
                dwAspect: DVASPECT_CONTENT.0,
                lindex: -1,
                tymed: TYMED_HGLOBAL.0 as u32,
            };
            assert!(unsafe { object.GetData(&wrong) }.is_err(), "非 CF_HDROP 不应当给数据");
        }
    }
}

#[cfg(not(windows))]
mod platform {
    //! 非 Windows:拖放没有对应实现,保持空桩(拖出返回人话错误)。

    use std::path::PathBuf;

    use super::DragEffect;

    pub(super) fn drag_out(
        _hwnd: isize,
        _paths: &[PathBuf],
        _allow_move: bool,
    ) -> Result<DragEffect, String> {
        Err("系统拖放只在 Windows 上实现".to_string())
    }
}