// design_view_actions.cpp — see design_view_actions.h.
//
// main.cpp owns the API pointers; this TU gets them extern. Action ids are minted
// from FOREVER-STABLE strings — never change one after ship.
//
// Each action mutates the session's ViewModeModel (membership tag/untag/show-both, or
// the active mode via toggle/activate), then reapplies the active mode through the
// view shell (applyMode) so the change takes visible effect immediately.
//
// Selection-driven mutations iterate the CURRENT REAPER track selection
// (CountSelectedTracks/GetSelectedTrack ignore the master, which is correct — the
// master is never tagged) and resolve each track to its canonical GUID key so the
// keys match what the view shell / view tree key on.

#include "shell/actions/design_view_actions.h"

#include <string>
#include <vector>

#include "shell/actions/action_registry.h"  // channelIdFor / registerAction (shared plumbing)

#include "core/view/lane_keys.h"         // view::isOnManualLane — the single managed/manual predicate
#include "core/view/view_mode_model.h"
#include "shell/persist/session.h"       // ReaSamplerSession (owns view() model)
#include "shell/capture/item_read.h"     // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
#include "shell/capture/track_guid.h"    // shared MediaTrack* -> canonical GUID key
#include "shell/panel/panel_window.h"    // bankPanelInvalidate — footer toggle repaint
#include "shell/view/view.h"             // applyMode + mintManagedLanes (D2 shell)

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountSelectedTracks
#define REAPERAPI_WANT_GetSelectedTrack
#define REAPERAPI_WANT_CountSelectedMediaItems
#define REAPERAPI_WANT_GetSelectedMediaItem
#define REAPERAPI_WANT_GetMediaItemTrack
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_Main_SaveProject
#define REAPERAPI_WANT_Help_Set
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"

namespace reasampler {

using view::isOnManualLane;

namespace {

// FOREVER-STABLE action-id SUFFIXES: the channel prefix is prepended at register
// (channelCommandId) — NEVER change a shipped suffix.
constexpr const char* kIdToggleMode      = "VIEW_TOGGLE_MODE";
constexpr const char* kIdActivateArrange = "VIEW_ACTIVATE_ARRANGE";
constexpr const char* kIdActivateDesign  = "VIEW_ACTIVATE_DESIGN";
constexpr const char* kIdTagDesign       = "VIEW_TAG_DESIGN";
constexpr const char* kIdTagArrange      = "VIEW_TAG_ARRANGE";
constexpr const char* kIdUntag           = "VIEW_UNTAG";
constexpr const char* kIdShowBoth        = "VIEW_SHOW_BOTH";
// Item-level mode moves — the item analog of the track tag family above.
constexpr const char* kIdMoveItemsDesign  = "VIEW_MOVE_ITEMS_DESIGN";
constexpr const char* kIdMoveItemsArrange = "VIEW_MOVE_ITEMS_ARRANGE";
constexpr const char* kIdUntagItems       = "VIEW_UNTAG_ITEMS";

// Not owned here (main.cpp owns g_session).
ReaSamplerSession* g_session = nullptr;

int g_cmdToggleMode = 0;
int g_cmdActivateArrange = 0;
int g_cmdActivateDesign = 0;
int g_cmdTagDesign = 0;
int g_cmdTagArrange = 0;
int g_cmdUntag = 0;
int g_cmdShowBoth = 0;
int g_cmdMoveItemsDesign = 0;
int g_cmdMoveItemsArrange = 0;
int g_cmdUntagItems = 0;

// gaccel storage must outlive registration — REAPER holds each pointer until we
// mirror-unregister it. One per action.
gaccel_register_t g_accelToggleMode{};
gaccel_register_t g_accelActivateArrange{};
gaccel_register_t g_accelActivateDesign{};
gaccel_register_t g_accelTagDesign{};
gaccel_register_t g_accelTagArrange{};
gaccel_register_t g_accelUntag{};
gaccel_register_t g_accelShowBoth{};
gaccel_register_t g_accelMoveItemsDesign{};
gaccel_register_t g_accelMoveItemsArrange{};
gaccel_register_t g_accelUntagItems{};

std::vector<std::string> selectedTrackGuids() {
    std::vector<std::string> guids;
    const int n = CountSelectedTracks(nullptr);  // nullptr = active project
    guids.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
    for (int i = 0; i < n; ++i) {
        MediaTrack* tr = GetSelectedTrack(nullptr, i);
        if (!tr) continue;
        std::string g = guidString(tr);
        if (!g.empty()) guids.push_back(std::move(g));
    }
    return guids;
}

// Reapplies the CURRENT active mode so a membership mutation takes visible effect
// immediately (park/unpark/re-derive parents).
void reapplyActiveMode() {
    applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
}

constexpr int kFreeModeFixedLanes = 2;  // I_FREEMODE value; mirrors the shell's constant

// Each selected item's GUID plus whether it sits on a MANUAL lane (EXEMPT — never
// retagged/re-laned). The lane name is read only on a fixed-lane track; on a normal
// track the shared predicate returns false for an empty name, so the read is skipped.
// Items whose GUID cannot be read are dropped (an empty GUID must never be retagged).
std::vector<RetagItem> selectedRetagItems() {
    std::vector<RetagItem> items;
    const int n = CountSelectedMediaItems(nullptr);  // nullptr = active project
    items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
    for (int i = 0; i < n; ++i) {
        MediaItem* it = GetSelectedMediaItem(nullptr, i);
        if (!it) continue;
        std::string g = itemGuid(it);
        if (g.empty()) continue;

        MediaTrack* tr = GetMediaItemTrack(it);
        const bool fixedLane =
            tr && static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
        const std::string laneNm = fixedLane ? itemLaneName(tr, it) : std::string{};
        items.push_back(RetagItem{std::move(g), isOnManualLane(fixedLane, laneNm)});
    }
    return items;
}

// Persists the bank + Design-View model after every state-changing action so the
// view model is not lost across save/close/reopen. If membership is non-empty and
// the project is unsaved, prompts Save-As first (mirrors the flow capture uses) —
// DAW-ONLY: Main_SaveProject(proj, true) blocks until the dialog is dismissed.
void persistViewState() {
    if (!g_session->view().membership().empty()) {
        ReaProject* proj = EnumProjects(-1, nullptr, 0);
        if (proj) {
            auto readRppPath = [&]() -> std::string {
                std::vector<char> buf(4096, '\0');
                EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
                return std::string(buf.data());
            };

            if (readRppPath().empty()) {
                Main_SaveProject(proj, true);
                if (readRppPath().empty()) {  // still empty -> user cancelled
                    ShowConsoleMsg(
                        "ReaSampler: Design View state will not persist until "
                        "the project is saved.\n");
                    return;
                }
            }
        }
    }
    g_session->saveToActiveProject();
}

// The footer segment already reads disabled while the transport runs, but an action can
// be fired with the panel closed — so the refusal also goes through Help_Set (SDK
// documents only the signature, not its display surface; inferred to be visible
// feedback by name, not confirmed). Guarded on the transport because applyMode's
// other refusal is an unregistered mode id.
void reportModeSwitchRefused() {
    if (transportBlocksModeSwitch(nullptr) && Help_Set)
        Help_Set("ReaSampler: stop the transport to switch view mode", true);
}

// Cycle to the next mode in ordinal order. applyMode itself sets the model's active
// mode, so we only compute the target and apply.
void doToggleMode() {
    const std::string target =
        nextModeId(g_session->view().modes(), g_session->view().activeModeId());
    if (target.empty()) return;  // no modes to cycle to (degenerate)
    if (!applyMode(g_session->view(), target, nullptr)) { reportModeSwitchRefused(); return; }
    persistViewState();
    bankPanelInvalidate();  // repaint the footer [Arrange|Design] toggle immediately
}

// Direct jump to a named mode. applyMode is a no-op (returns false, no mutation) if
// the id is unregistered or the transport is running, so both fail safe.
void doActivateMode(const std::string& modeId) {
    if (!applyMode(g_session->view(), modeId, nullptr)) { reportModeSwitchRefused(); return; }
    persistViewState();
    bankPanelInvalidate();  // repaint the footer [Arrange|Design] toggle immediately
}

// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
// cross-mode case is show-both).
void doTag(const std::string& modeId) {
    for (const std::string& g : selectedTrackGuids())
        g_session->view().membership().tag(g, modeId);
    reapplyActiveMode();
    persistViewState();
}

// Shared body behind "Untag selected" and "Tag -> Arrange" — Arrange is the absence
// of a tag, so the two actions are the same act.
void doUntag() {
    for (const std::string& g : selectedTrackGuids())
        g_session->view().membership().untag(g);
    reapplyActiveMode();
    persistViewState();
}

// Flips each track's pin independently — the honest semantics of a toggle on a
// multi-selection (a mixed selection converges toward uniform only if it already was).
void doShowBoth() {
    MembershipIndex& m = g_session->view().membership();
    for (const std::string& g : selectedTrackGuids())
        m.setShowBoth(g, !m.isShowBoth(g));
    reapplyActiveMode();
    persistViewState();
}

// Retag the current ITEM selection to `targetMode` (empty => untag -> Arrange
// default). planItemRetag decides which items to retag (manual-lane items are
// EXEMPT), upholding the managed-lanes-only invariant. Wrapped in ONE Undo block.
//
// UNDO/SAVE ordering: persistViewState may pop a Save-As dialog, which must NOT sit
// inside the Undo block, so we close the block first, then persist.
void doMoveItems(const std::string& targetMode) {
    const std::vector<RetagItem> selected = selectedRetagItems();
    const std::vector<ItemRetagOp> ops = planItemRetag(selected, targetMode);
    if (ops.empty()) return;  // nothing selected, or every selected item was exempt/empty

    MembershipIndex& membership = g_session->view().membership();

    Undo_BeginBlock2(nullptr);
    for (const ItemRetagOp& op : ops) {
        if (op.untag) membership.untag(op.guid);
        else          membership.tag(op.guid, op.modeId);
    }
    // Re-drive the same minting/apply path auto-tag uses: mint/split lanes for any
    // track whose items now span modes, then reassert active-mode lane visibility.
    mintManagedLanes(g_session->view(), nullptr);
    reapplyActiveMode();

    const std::string label =
        targetMode.empty()
            ? std::string("ReaSampler: untag selected items")
            : std::string("ReaSampler: move selected items -> ") + targetMode;
    Undo_EndBlock2(nullptr, label.c_str(), -1);

    persistViewState();
}

}  // namespace

void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
    g_session = session;

    g_cmdToggleMode = registerAction(rec, kIdToggleMode, g_accelToggleMode,
                                     "toggle Design View mode");
    g_cmdActivateArrange = registerAction(rec, kIdActivateArrange, g_accelActivateArrange,
                                          "activate mode Arrange");
    g_cmdActivateDesign = registerAction(rec, kIdActivateDesign, g_accelActivateDesign,
                                         "activate mode Design");
    g_cmdTagDesign = registerAction(rec, kIdTagDesign, g_accelTagDesign,
                                    "tag selected tracks -> Design");
    g_cmdTagArrange = registerAction(rec, kIdTagArrange, g_accelTagArrange,
                                     "tag selected tracks -> Arrange");
    g_cmdUntag = registerAction(rec, kIdUntag, g_accelUntag,
                                "untag selected tracks");
    g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth,
                                   "show both for selected tracks");

    g_cmdMoveItemsDesign = registerAction(rec, kIdMoveItemsDesign, g_accelMoveItemsDesign,
                                          "move selected items -> Design");
    g_cmdMoveItemsArrange = registerAction(rec, kIdMoveItemsArrange, g_accelMoveItemsArrange,
                                           "move selected items -> Arrange");
    g_cmdUntagItems = registerAction(rec, kIdUntagItems, g_accelUntagItems,
                                     "untag selected items");
}

bool designViewHandleCommand(int command) {
    if (command == 0 || !g_session) return false;

    if (command == g_cmdToggleMode)      { doToggleMode(); return true; }
    if (command == g_cmdActivateArrange) { doActivateMode(kArrangeModeId); return true; }
    if (command == g_cmdActivateDesign)  { doActivateMode(kDesignModeId);  return true; }
    if (command == g_cmdTagDesign)       { doTag(kDesignModeId);  return true; }
    if (command == g_cmdTagArrange)      { doUntag(); return true; }
    if (command == g_cmdUntag)           { doUntag(); return true; }
    if (command == g_cmdShowBoth)        { doShowBoth(); return true; }

    if (command == g_cmdMoveItemsDesign)  { doMoveItems(kDesignModeId); return true; }
    if (command == g_cmdMoveItemsArrange) { doMoveItems(std::string{}); return true; }
    if (command == g_cmdUntagItems)       { doMoveItems(std::string{}); return true; }

    return false;  // not ours — caller's hookcommand keeps looking
}

void designViewUnregisterActions(reaper_plugin_info_t* rec) {
    // Reverse registration order; each '-command_id' re-presents the SAME interned
    // pointer channelIdFor returned above.
    rec->Register("-gaccel", (void*)&g_accelUntagItems);
    rec->Register("-command_id", (void*)channelIdFor(kIdUntagItems));
    rec->Register("-gaccel", (void*)&g_accelMoveItemsArrange);
    rec->Register("-command_id", (void*)channelIdFor(kIdMoveItemsArrange));
    rec->Register("-gaccel", (void*)&g_accelMoveItemsDesign);
    rec->Register("-command_id", (void*)channelIdFor(kIdMoveItemsDesign));
    rec->Register("-gaccel", (void*)&g_accelShowBoth);
    rec->Register("-command_id", (void*)channelIdFor(kIdShowBoth));
    rec->Register("-gaccel", (void*)&g_accelUntag);
    rec->Register("-command_id", (void*)channelIdFor(kIdUntag));
    rec->Register("-gaccel", (void*)&g_accelTagArrange);
    rec->Register("-command_id", (void*)channelIdFor(kIdTagArrange));
    rec->Register("-gaccel", (void*)&g_accelTagDesign);
    rec->Register("-command_id", (void*)channelIdFor(kIdTagDesign));
    rec->Register("-gaccel", (void*)&g_accelActivateDesign);
    rec->Register("-command_id", (void*)channelIdFor(kIdActivateDesign));
    rec->Register("-gaccel", (void*)&g_accelActivateArrange);
    rec->Register("-command_id", (void*)channelIdFor(kIdActivateArrange));
    rec->Register("-gaccel", (void*)&g_accelToggleMode);
    rec->Register("-command_id", (void*)channelIdFor(kIdToggleMode));

    g_session = nullptr;
}

}  // namespace reasampler
