// insert.cpp — REAPER-facing placement shell (M6). 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 (CLAUDE.md §contract).
//
// THIS IS THE INTENDED PLACEMENT PATH. Unlike capture / bank_panel (which never
// touch the arrange), insert deliberately adds items to the arrange — that is its
// whole job (CONTEXT.md §load-bearing principle). It runs ONLY from its own action.
//
// FLAGGED RUNTIME ASSUMPTIONS (semantics the header does not fully specify — must
// be DAW-verified by Daniel post-merge; see the handoff):
//   A. InsertMedia base mode 0 ("add to current track") targets the track that is
//      currently the ONLY selected track. The header names the base target but does
//      not spell out how "current track" resolves at runtime. We force exactly one
//      selected track via SetOnlyTrackSelected before each InsertMedia call, which
//      is the most defensible interpretation; if REAPER uses a different notion of
//      "current" (e.g. last-focused, not last-selected), DAW-verify and adjust.
//   B. InsertMedia mode 0 inserts AT THE EDIT CURSOR. Placement at the edit cursor
//      is REAPER's documented convention for base modes 0/1 (the header does not
//      spell out an explicit "at edit cursor" bit). Flagged for DAW-verification.
//   C. InsertMedia ADVANCES the edit cursor to the end of the inserted media. We
//      reset the cursor to the snapshot position before EACH track's insert, so
//      assumption C's truth or falsity is irrelevant: we own the cursor reset.
//   D. SetEditCurPos(time, false, false) moves the cursor without scrolling the view
//      and without seeking the transport. The header lists the args as
//      (time, moveview, seekplay) — moveview=false and seekplay=false are the
//      non-disruptive choice; flagged in case the DAW shows otherwise.
//   E. SetOnlyTrackSelected deselects all tracks and selects exactly one. The header
//      doc-comment says "Set exactly one track selected, deselect all others" —
//      this is the strongest confirmation we have; flagged for DAW-verification.

#include "insert.h"

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

#include "bank_model.h"
#include "bank_panel.h"
#include "capture_paths.h"
#include "persist.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 {

namespace {

namespace fs = std::filesystem;

// The current project's directory (mirrors bank_panel/capture/persist). The bank
// index stores relative paths; resolving a bank file needs the current .rpp dir.
// FOLLOW-UP (already noted in bank_panel.cpp): a shared "current project dir"
// REAPER helper is a clean small refactor now that a fourth consumer exists — out
// of scope for M6.
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 the user's currently-selected track set (ignores master, matches
// CountSelectedTracks / GetSelectedTrack which both skip master). Returns the
// tracks in selection order so we can restore the original state afterward.
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;
}

// Restore a previously-snapshotted track selection: deselect all (by setting the
// first track alone) then re-select the full set. If the snapshot is empty we
// leave all tracks deselected; no-op guard handles a completely empty project.
void restoreSelectedTracks(const std::vector<MediaTrack*>& tracks) {
    if (tracks.empty()) return;
    // Deselect all via the first track, then re-add the rest.
    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 to target: the user's currently-selected track set. No-op (with a clear
    // console message) when nothing is selected — inserting without a target track
    // would create an unintended new 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 to place: the single focused sample from the panel. Multi-select is
    // deprioritized; take the first (or only) selected id. An empty panel selection
    // is a no-op — nothing to place.
    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 the bank lives on disk. An unsaved project has no resolvable bank dir;
    // insert is a no-op rather than resolving against CWD (CLAUDE.md invariant).
    const std::string projectDir = currentProjectDir();
    if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; }

    // Resolve the id against the bank the SELECTION came from — under B4's vertical
    // split the selection may live in the pool or a shown named bank, which is 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 BankIndex* srcIndex = session->book().index(srcBankId);
    const BankIndex& 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();

    // Wrap the whole placement (all tracks + selection/cursor save-restore) in ONE
    // undo block so a single undo removes every item and restores the state before
    // the action. Opened before the first InsertMedia, closed after the restore,
    // unconditionally — the block is always balanced.
    Undo_BeginBlock2(nullptr);

    // Insert onto EACH selected track at the SAME edit-cursor position (assumption B).
    // For each track: isolate it as the only selection so InsertMedia mode 0 targets
    // it unambiguously (assumption A + E), reset the cursor to the snapshot position
    // (assumption C cursor advance is irrelevant — 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 user's original track selection and cursor position so the action
    // is non-destructive to their DAW state (non-negotiable per the brief).
    restoreSelectedTracks(selectedTracks);
    SetEditCurPos(cursorPos, false, false);

    // Label reflects the count and the conform choice so the undo history reads
    // clearly ("ReaSampler: insert on 2 tracks" etc.). extraflags -1 = UNDO_STATE_ALL
    // (superset: 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
