// insert.cpp — REAPER-facing placement shell. See insert.h.
//
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
// extern.
//
// Unlike capture / bank_panel (which never touch the arrange), insert deliberately
// adds items to the arrange — that is its whole job. Runs only from its own action.
//
// Runtime assumptions the SDK header doesn't fully spell out (flagged, not yet
// DAW-verified):
//   A. InsertMedia mode 0 ("add to current track") is assumed to target the sole
//      selected track — the header doesn't spell out how "current" resolves, so we
//      force exactly one selection via SetOnlyTrackSelected before each call. If
//      REAPER means last-focused rather than last-selected, this needs revisiting.
//   B. Mode 0 is assumed to insert at the edit cursor (REAPER's documented
//      convention for base modes 0/1; the header has no explicit "at cursor" bit).
//   C. InsertMedia may advance the cursor to the end of the inserted media; we
//      reset to the snapshot position before each track's insert, so this doesn't
//      matter either way.
//   D. SetEditCurPos(time, false, false) — moveview=false, seekplay=false — is
//      assumed to move the cursor without scrolling the view or the transport.
//   E. SetOnlyTrackSelected deselects all tracks and selects exactly one (per its
//      header doc-comment — the strongest confirmation we have here).

#include "shell/capture/insert.h"

#include <filesystem>
#include <string>
#include <vector>

#include "core/model/bank_model.h"
#include "shell/panel/panel_bank_ops.h"  // bankPanelSelectedSampleIds / SourceBankId
#include "core/capture/capture_paths.h"
#include "shell/persist/session.h"

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

namespace reasampler {

using capture::computeInsertMode;
using capture::normalizeSlashes;
using capture::resolveBankFile;
using capture::TempoConform;

namespace {

namespace fs = std::filesystem;

// Mirrors bank_panel/capture/persist's own derivation; the bank index stores
// relative paths, so resolving a file needs the current .rpp dir. A shared "current
// project dir" helper would be a clean small refactor now that a fourth consumer
// exists (also noted in panel_bank_ops.cpp) — out of scope here.
std::string currentProjectDir() {
    std::vector<char> buf(4096, '\0');
    EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
    std::string rpp(buf.data());
    if (rpp.empty()) return {};  // unsaved project: no resolvable bank
    return normalizeSlashes(fs::path(rpp).parent_path().string());
}

// Snapshot of the currently-selected track set (master is skipped, matching
// CountSelectedTracks/GetSelectedTrack), in selection order, for restore later.
std::vector<MediaTrack*> snapshotSelectedTracks() {
    const int n = CountSelectedTracks(nullptr);  // nullptr = active project
    std::vector<MediaTrack*> tracks;
    tracks.reserve(static_cast<size_t>(n));
    for (int i = 0; i < n; ++i)
        tracks.push_back(GetSelectedTrack(nullptr, i));
    return tracks;
}

// Restores a snapshotted selection: deselect all via the first track, then
// re-select the rest. Empty snapshot -> no-op (guards an empty project).
void restoreSelectedTracks(const std::vector<MediaTrack*>& tracks) {
    if (tracks.empty()) return;
    SetOnlyTrackSelected(tracks[0]);
    for (size_t i = 1; i < tracks.size(); ++i)
        SetTrackSelected(tracks[i], true);
}

}  // namespace

InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request) {
    InsertResult result;
    if (!session) { result.status = InsertStatus::NoSelection; return result; }

    // WHO: the user's selected track set. No-op when nothing is selected — inserting
    // without a target track would create an unintended track or behave unpredictably.
    const std::vector<MediaTrack*> selectedTracks = snapshotSelectedTracks();
    if (selectedTracks.empty()) {
        ShowConsoleMsg("ReaSampler insert: select a track first.\n");
        result.status = InsertStatus::NoSelection;
        return result;
    }

    // WHAT: the single focused sample from the panel; multi-select is deprioritized,
    // so take the first id. Empty selection -> no-op.
    const std::vector<std::string> ids = bankPanelSelectedSampleIds();
    if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; }
    const std::string& id = ids.front();  // focused / first selected — single sample

    // WHERE: an unsaved project has no resolvable bank dir; no-op rather than
    // resolving against CWD.
    const std::string projectDir = currentProjectDir();
    if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; }

    // Resolve against the bank the selection came from — it may be the pool or a
    // shown named bank, not necessarily the active/capture-target bank. Fall back to
    // the active bank when the source id names no bank (defensive).
    const std::string srcBankId = bankPanelSelectedSourceBankId();
    const BankModel* srcIndex = session->book().index(srcBankId);
    const BankModel& bank = srcIndex ? *srcIndex : session->bank();
    const Sample* sample = bank.query(id);
    if (!sample) { result.status = InsertStatus::NothingResolved; return result; }

    const std::string abs = resolveBankFile(projectDir, sample->relativePath);
    if (abs.empty() || !fs::exists(fs::path(abs))) {
        result.status = InsertStatus::NothingResolved;
        return result;
    }

    const int mode = computeInsertMode(request.options);

    // Snapshot the edit cursor position up front so we can restore it to the same
    // position for each track insert (and after the whole operation).
    const double cursorPos = GetCursorPosition();

    // One undo block around the whole placement (all tracks + selection/cursor
    // restore) so a single undo removes every item and restores prior state. Always
    // balanced — opened before the first insert, closed after the restore.
    Undo_BeginBlock2(nullptr);

    // Insert onto each selected track at the same cursor position (assumption B): per
    // track, isolate it as the only selection (A + E), reset the cursor (C is
    // irrelevant since we own the reset), then insert.
    for (MediaTrack* track : selectedTracks) {
        SetOnlyTrackSelected(track);                       // assumption A + E
        SetEditCurPos(cursorPos, false, false);            // assumption D
        InsertMedia(abs.c_str(), mode);
        ++result.inserted;
    }

    // Restore the original selection + cursor — non-destructive to the user's DAW state.
    restoreSelectedTracks(selectedTracks);
    SetEditCurPos(cursorPos, false, false);

    // Label reflects count + conform choice for a clear undo history. extraflags -1 =
    // UNDO_STATE_ALL (tracks, items, envelope points, project state).
    const std::string label =
        "ReaSampler: insert on " + std::to_string(result.inserted) +
        (result.inserted == 1 ? " track" : " tracks") +
        (request.options.conform == TempoConform::None ? "" : " (conform)");
    Undo_EndBlock2(nullptr, label.c_str(), -1);

    result.status = InsertStatus::Ok;
    return result;
}

} // namespace reasampler
