#pragma once
// drag_out — the REAPER-free / OS-free decision logic behind the bank_panel's native OS
// drag-out (Milestone 11, the final polish point). Two pure concerns live here so they are
// unit-tested outside the DAW (CLAUDE.md §load-bearing split); the OLE / SWELL initiation
// and the bank_panel gesture hook stay in the shell (drag_out_win.* + bank_panel.cpp).
//
//   1. GESTURE BOUNDARY (invariant #4 — do not regress the internal drag). The panel
//      already runs an INTERNAL drag: press a selected cell, cross a threshold, drop onto
//      a pool/banks region or a tab to move/copy the samples between banks. That drag lives
//      entirely INSIDE the panel client rect. The OS drag is a DISTINCT gesture with a
//      distinct, discoverable boundary: while a drag is armed with samples in the payload,
//      the moment the pointer LEAVES the panel client area the gesture becomes OS-bound —
//      the payload is being dragged out to another window / Explorer / another DAW. Inside
//      the client area it stays internal; with no armed samples there is no drag at all.
//      This function is that decision, pure over (drag state + pointer + panel rect).
//
//   2. PATH-LIST ASSEMBLY. The OS drop carries absolute file paths (Windows CF_HDROP /
//      macOS file-list pasteboard). Turning the armed sample ids into that path list —
//      resolving each id to its already-on-disk bank file, de-duping, and applying an
//      explicit skip-missing-file policy — is pure string work over a resolver the shell
//      supplies (the shell owns the REAPER project-dir read + resolveBankFile; this module
//      owns the set algebra and the result contract). NO temp files: the bank files already
//      exist; the list points straight at them (COPY-ONLY is enforced at the OS layer — see
//      drag_out_win — never by relocating or copying bytes here).
//
// PURE MODULE: NO REAPER types, NO SWELL, NO OS/OLE, NO vendor/ includes. Standard library
// only. Builds and unit-tests without REAPER. Mirror of action_buttons / mode_switch.

#include <string>
#include <vector>

namespace reasampler {

// --- Gesture boundary ---------------------------------------------------------

// The panel's client rectangle in its own client coordinates (top-left origin, the SWELL/
// LICE convention). width/height are the extents; a point (px, py) is INSIDE when
// x <= px < x + width and y <= py < y + height (half-open, matching the panel's other
// hit-tests so the edge is claimed consistently).
struct PanelClientRect {
    int x = 0;
    int y = 0;
    int width = 0;
    int height = 0;

    bool operator==(const PanelClientRect& o) const {
        return x == o.x && y == o.y && width == o.width && height == o.height;
    }
};

// The live drag state the shell tracks, reduced to what the boundary decision needs:
// whether a drag is currently active (threshold crossed) and whether the armed payload
// carries at least one sample. (Pre-threshold "armed but not yet dragging" is NOT a drag
// for this decision — the shell only asks once a drag is under way.)
//
// S17 (drop-and-load) adds two inputs that refine the OUTSIDE-the-panel decision without
// touching the INSIDE decision (the internal bank-to-bank drag stays byte-identical):
//   * singleCapture — the payload holds EXACTLY ONE sample id. Only a single-capture drag
//     arms the InstrumentDrop gesture (per the S17 open-question lean: a multi-capture drag
//     over an FX button is NOT an instrument drop — it falls through to OsDrag, the natural
//     multi-file drag-out to Explorer/another DAW). REJECT, not load-first: the whole gesture
//     is "make ONE capture a playable instrument", so a multi payload is out of contract here.
//   * overReaperUi — a SHELL-SUPPLIED predicate: true when the pointer, though outside the
//     panel client rect, is still over REAPER's OWN window/UI (the shell owns the REAPER
//     hit query, e.g. GetThingFromPoint; the pure layer owns only the set/boundary algebra).
// Both default false, so an M11-era caller that fills only {dragging, hasArmedSamples} gets
// EXACTLY the M11 behavior: outside the client rect with overReaperUi=false -> OsDrag.
struct DragState {
    bool dragging = false;       // threshold crossed; a drag is in progress
    bool hasArmedSamples = false; // the drag payload holds >= 1 sample id
    bool singleCapture = false;  // S17: payload holds EXACTLY one sample (arms InstrumentDrop)
    bool overReaperUi = false;   // S17: pointer is over REAPER's own UI (shell-supplied)
};

// What the shell should do with the drag given the current pointer position.
enum class DragGesture {
    None,           // no drag under way, or an empty payload — do nothing
    Internal,       // dragging inside the panel — the existing bank-to-bank move/copy drag
    InstrumentDrop, // S17: single-capture drag left the panel but is over REAPER's UI —
                    // the shell hover-tracks the TCP FX button and, on release, adds a
                    // ReaSampler 9000 instance preloaded with the dragged capture.
    OsDrag,         // dragging with samples, pointer left REAPER entirely — hand off to the OS
};

// Decides the gesture for a drag at pointer (px, py) over `client`, given `state`.
//   * Not dragging (or no armed samples): None — the shell ignores the move.
//   * Dragging with samples, pointer INSIDE the client rect: Internal — unchanged
//     bank-to-bank behavior (invariant #4: the internal drag stays byte-identical).
//   * Dragging OUTSIDE the client rect, SINGLE capture, over REAPER's UI: InstrumentDrop —
//     the drag is heading for a track's FX button (S17); the shell hover-tracks + highlights.
//   * Dragging OUTSIDE the client rect otherwise (multi-capture, OR the pointer has left
//     REAPER entirely): OsDrag — the samples are leaving to the OS; the shell initiates the
//     native OS drag with the resolved paths.
// The INSIDE decision is untouched (M11 internal drag is byte-identical). The M11 boundary
// (left the client rect -> OsDrag) is REFINED, not replaced: leaving the rect now asks
// "single-capture and over REAPER's UI -> InstrumentDrop, else -> OsDrag" — so the M11
// OS-drag-out (multi payload, or pointer off REAPER) keeps its exact behavior. Position-only
// + state-only (no hidden state), so re-entry back inside returns Internal.
DragGesture decideGesture(int px, int py, const PanelClientRect& client,
                          const DragState& state);

// --- Path-list assembly -------------------------------------------------------

// One armed sample reduced to what path assembly needs: the resolved ABSOLUTE file path
// the shell computed for it (empty when the shell could not resolve it — e.g. no project
// dir / empty relative path). The shell resolves each via the SAME machinery the panel
// already uses for audition/insert (resolveBankFile over the current project dir), so the
// drag points at the real bank file — no temp copy.
struct ResolvedSample {
    std::string absolutePath;  // resolved absolute path, or "" when unresolvable
    bool fileExists = false;   // shell stat() result — drives the skip-missing policy
};

// The outcome of assembling the drag's path list: the de-duped, existing-only absolute
// paths to hand to the OS, plus explicit tallies so the shell can decide whether to
// initiate at all (an empty `paths` means nothing draggable — do NOT start a drag).
struct PathList {
    std::vector<std::string> paths;  // de-duped, existing files, in first-seen order
    int skippedMissing = 0;          // resolved but file did not exist (skip policy)
    int skippedUnresolved = 0;       // shell could not resolve a path at all
    int skippedDuplicate = 0;        // same absolute path seen more than once
};

// Assembles the drag path list from the resolved samples (in selection order).
// Policy (all explicit, all tested):
//   * SKIP-MISSING: a sample whose file does not exist on disk is skipped (counted in
//     skippedMissing) — a stale index entry must never put a dangling path on the OS
//     clipboard. This is the deliberate skip policy the brief asks be made explicit.
//   * SKIP-UNRESOLVED: an empty absolutePath (shell could not resolve) is skipped
//     (skippedUnresolved) — same reasoning, no empty entry reaches the OS.
//   * DEDUPE: the same absolute path appearing twice (two index entries, one file — the
//     cross-bank copy case) yields ONE CF_HDROP entry (skippedDuplicate counts the extras),
//     so the OS never sees a duplicate drop path. First occurrence wins; order preserved.
//   * EMPTY SELECTION: an empty input yields an empty PathList (all tallies zero) — the
//     shell reads paths.empty() and does not start a drag.
// Comparison is exact-string (the shell normalizes slashes/case upstream if it wants
// case-insensitive dedup on Windows — the pure layer does not guess a platform rule).
PathList assemblePathList(const std::vector<ResolvedSample>& resolved);

} // namespace reasampler
