// arrange_drop_win.cpp — see arrange_drop_win.h. main.cpp owns the API pointers; this TU gets
// them extern via the WANT list.
//
// Runtime assumptions, all DAW-verifiable and none confirmed by the SDK header:
//   A. InsertMedia base mode 0 targets the sole selected track and inserts at the edit cursor;
//      SetOnlyTrackSelected isolates that track first (the same pair insert.cpp relies on).
//   B. InsertMedia advances the edit cursor past the media it added. That advance IS the
//      multi-file layout: the cursor is deliberately not reset between files, so N captures
//      land end to end. If REAPER does not advance it, they stack at one position instead —
//      visible and one Ctrl-Z away, never silent. insert.cpp does NOT share this assumption: it
//      resets the cursor before every track's insert specifically to stay independent of
//      cursor-advance behavior (insert.cpp:18-20, "this doesn't matter either way"). This call
//      site is the first in the tree to depend on it.
//   C. SnapToGrid honors the project's snap-enabled toggle. The header documents no
//      snap-enabled query for the arrange, so a drop taken with snapping OFF is the test that
//      settles it.
//   D. GetSet_ArrangeView2's one-pixel span [screenX, screenX+1) reads the time at that column.
//      The header documents only the all-zero span (screen_x_start==screen_x_end==0) as the
//      "whole view" special case; the per-column reading for any other span is inferred, not
//      documented.
//   E. InsertMedia's int return isn't SDK-documented; treated conservatively as 0=failure,
//      nonzero=success — performArrangeDrop counts only the latter.

#include "shell/actions/arrange_drop_win.h"

#include <cstddef>
#include <cstdio>
#include <string>
#include <vector>

#include "core/capture/insert_plan.h"  // computeInsertMode — the ONE InsertMedia bitfield owner

#include "reaper_plugin.h"

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountSelectedTracks
#define REAPERAPI_WANT_GetCursorPosition
#define REAPERAPI_WANT_GetSelectedTrack
#define REAPERAPI_WANT_GetSet_ArrangeView2
#define REAPERAPI_WANT_InsertMedia
#define REAPERAPI_WANT_SetEditCurPos
#define REAPERAPI_WANT_SetOnlyTrackSelected
#define REAPERAPI_WANT_SetTrackSelected
#define REAPERAPI_WANT_SnapToGrid
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"

namespace reasampler {

using capture::computeInsertMode;
using capture::InsertOptions;

namespace {

// Selection snapshot/restore, so a drop leaves the user's track selection exactly as it found
// it. Mirrors insert.cpp's pair; kept separate here because insert.cpp sits in the capture
// pillar, outside this track's surface fence — not a ruling that the two should never share a
// helper, just not this track's call to make.
//
// CountSelectedTracks/GetSelectedTrack both skip the master track (SDK header), so a user with
// the master selected loses that selection across the drop — pre-existing behavior inherited
// from insert.cpp's identical pair, not fixed here.
std::vector<MediaTrack*> snapshotSelectedTracks() {
    const int n = CountSelectedTracks(nullptr);  // nullptr = active project
    std::vector<MediaTrack*> tracks;
    tracks.reserve(static_cast<std::size_t>(n));
    for (int i = 0; i < n; ++i) tracks.push_back(GetSelectedTrack(nullptr, i));
    return tracks;
}

void restoreSelectedTracks(const std::vector<MediaTrack*>& tracks) {
    if (tracks.empty()) return;  // nothing was selected; leave whatever the drop selected
    SetOnlyTrackSelected(tracks[0]);
    for (std::size_t i = 1; i < tracks.size(); ++i) SetTrackSelected(tracks[i], true);
}

}  // namespace

double arrangeTimeAtScreenX(int screenX) {
    double start = 0.0, end = 0.0;
    // isSet=false with a one-pixel span [screenX, screenX+1) is assumed to read the time at
    // that column — inferred, not documented (assumption D in the file header). The SDK's ONLY
    // documented special form is screen_x_start==screen_x_end==0 (both zero) for "the whole
    // arrange view's start/end time"; a zero-width span at a nonzero column (e.g. screenX,
    // screenX) is NOT that special case, so the +1 here is precautionary rather than required.
    GetSet_ArrangeView2(nullptr, false, screenX, screenX + 1, &start, &end);
    return start < 0.0 ? 0.0 : start;
}

int performArrangeDrop(MediaTrack* track, double time,
                       const std::vector<std::string>& absolutePaths) {
    if (!track || absolutePaths.empty()) return 0;

    const std::vector<MediaTrack*> priorSelection = snapshotSelectedTracks();
    const double priorCursor = GetCursorPosition();
    const int mode = computeInsertMode(InsertOptions{});  // current track, native length, no stretch

    // One undo block around the whole drop (every item plus the selection/cursor restore) so a
    // single Ctrl-Z returns the project to exactly its pre-drop state.
    Undo_BeginBlock2(nullptr);

    SetOnlyTrackSelected(track);
    SetEditCurPos(SnapToGrid(nullptr, time), /*moveview=*/false, /*seekplay=*/false);

    int inserted = 0;
    for (const std::string& path : absolutePaths) {
        // No cursor reset between files — see assumption B in the file header. InsertMedia's
        // return isn't SDK-documented (assumption E); treated conservatively as 0=failure, so a
        // REAPER-side refusal is reflected in the count and in the undo label, not silent.
        if (InsertMedia(path.c_str(), mode) != 0) ++inserted;
    }

    // A fixed stack buffer, not std::string concatenation: the prior shape built the label with
    // std::to_string + `+` between the restore below and Undo_EndBlock2, so a bad_alloc there
    // would leave an unbalanced undo block open. snprintf here removes the allocation outright.
    char label[64];
    std::snprintf(label, sizeof(label), "ReaSampler: drop %d %s onto arrange", inserted,
                 inserted == 1 ? "capture" : "captures");

    restoreSelectedTracks(priorSelection);
    SetEditCurPos(priorCursor, /*moveview=*/false, /*seekplay=*/false);

    // extraflags -1 = UNDO_STATE_ALL, matching the insert action's own block.
    Undo_EndBlock2(nullptr, label, -1);
    return inserted;
}

}  // namespace reasampler
