// main.cpp — the SINGLE translation unit that OWNS the REAPER API pointers.
//
// REAPER dlopen()s reaper_*.dll|dylib|so from UserPlugins/ and calls the exported
// ReaperPluginEntry, handing over `rec` (rec->GetFunc resolves API pointers,
// rec->Register plugs our callbacks in). Exactly ONE .cpp defines
// REAPERAPI_IMPLEMENT (this one) — that allocates storage for the global API
// pointers every other TU gets `extern`. Never let a second TU define it.
//
// This TU is ONLY pointers + entry + dispatch. Its own action family registers
// through the data-driven table below (buildMainActionTable + action_registry) —
// adding a bindable action means adding ONE row and its handler function (OCP). The
// design_view / bank / ingest families keep their own register/handle/unregister
// triples, called from entry.

#define REAPERAPI_IMPLEMENT
#include "reaper_plugin.h"
#include "reaper_plugin_functions.h"

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

#include "core/capture/render_settings.h"        // captureActionTable
#include "core/version/app_version.h"            // appVersion
#include "shell/actions/ingest.h"
#include "shell/actions/action_registry.h"       // the registration table
#include "shell/actions/bank_actions.h"          // multi-bank action family
#include "shell/actions/design_view_actions.h"   // Design View action family
#include "shell/actions/package_export_action.h" // bank-package export action body
#include "shell/actions/package_import_action.h" // bank-package import action body
#include "core/wire/bake_wire.h"                 // kBakeActionSuffix (the shared action id)
#include "shell/capture/bake_land.h"             // resample-bake landing action body
#include "shell/capture/capture_batch.h"         // batch + recapture action bodies
#include "shell/capture/capture_orchestrator.h"  // single-capture / realtime / insert action bodies
#include "shell/capture/realtime_lifecycle.h"    // in-flight realtime state + tick driver
#include "shell/capture/render_in_place.h"       // render-in-place action body
#include "shell/panel/panel_input.h"             // bankPanelRefresh / bankPanelNotifyProjectLoaded
#include "shell/panel/panel_window.h"            // panel lifecycle (init/toggle/open-query/shutdown)
#include "shell/persist/session.h"               // ReaSamplerSession
#include "shell/view/view.h"                     // reconcileManagedLanes / applyMode

namespace capture = reasampler::capture;

// Globals other files reference via `extern`.
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr;
reaper_plugin_info_t*   g_rec   = nullptr;

// Retired command-id SUFFIXES: kept ONLY to mirror-unregister on unload so a user's
// stale keybindings are cleaned up. Never re-register these. The four-mode WET ids,
// the removed master scope/realtime actions, and the removed per-action tail variants
// (tail is now a panel toggle, not a paired action).
static const char* const kRetiredCaptureCmdSuffixes[] = {
    "CAPTURE_TRACKS_WET",
    "CAPTURE_ITEMS_WET",
    "CAPTURE_RAZOR_WET",
    "CAPTURE_MASTER",
    "CAPTURE_MASTER_REALTIME",
    "CAPTURE_ITEM_TAIL",
    "CAPTURE_TRACK_TAIL",
};

// Owns the in-memory BankModel and bridges it to project ext state. A timer tick
// drives g_session.poll() to detect project load / Save-As; capture adds Samples to
// g_session.bank() (resolves to the active bank's index), and we serialize the book
// back into the active project's ext state (the `banks` key) so it travels with the .rpp.
static reasampler::ReaSamplerSession g_session;

// Command id of the TOGGLE_BANK_PANEL row, resolved from the table once at load so
// OnToggleAction's checked-state poll is a single int compare (no per-poll lookup).
static int g_cmdToggleBankPanel = 0;

// Each handler is a thin stateless routing shim: (session, per-row arg) -> the
// action body in shell/capture/ or shell/panel/, existing only so table rows can be
// plain data with flat function pointers.

// `arg` is the captureActionTable() row index — the table rows below are built by
// iterating that pure taxonomy, so the routing stays 1:1 by construction.
static void RunCaptureScopeRow(int arg) {
    capture::RunCapture(g_session,
                        capture::captureActionTable()[static_cast<std::size_t>(arg)]);
}
static void RunToggleBankPanel(int)   { reasampler::bankPanelToggle(); }
static void RunCaptureItemAssign(int) { capture::RunCaptureItemAssign(g_session); }
// `arg` != 0 is the EXPLICIT conform-to-project-tempo opt-in (never silent); 0
// inserts at native length.
static void RunInsertSelected(int arg) {
    capture::RunInsertSelected(g_session, arg != 0);
}
static void RunBatchCaptureItems(int) { capture::RunBatchCaptureItems(g_session); }
static void RunBatchCaptureRazor(int) { capture::RunBatchCaptureRazor(g_session); }
static void RunCaptureRealtime(int)   { capture::RunCaptureRealtimeTrack(g_session); }
static void RunCancelRealtime(int)    { capture::RunCancelRealtime(g_session); }
static void RunRecaptureFromSource(int) { capture::RunRecaptureFromSource(g_session); }
static void RunRenderTrackInPlace(int) { capture::RunRenderTrackInPlace(g_session); }
static void RunResampleBake(int)      { capture::RunResampleBake(g_session); }
static void RunExportBankPackage(int) { reasampler::doBankPackageExport(g_session, g_session.book().activeBankId()); }
static void RunImportBankPackage(int) { reasampler::doImportBankPackage(g_session); }
static void RunShowVersion(int) {
    // On-demand only — no unconditional startup print (routine console chatter pops
    // the console window).
    ShowConsoleMsg(("ReaSampler " + reasampler::version::appVersion() + "\n").c_str());
}

// ONE row per bindable action this TU owns: FOREVER-STABLE id suffix, Actions-list
// phrase, handler, per-row arg. Registration, hookcommand dispatch, and the unload
// mirror-unregister all iterate this data. The capture scope rows come first,
// sourced from the pure captureActionTable() taxonomy; the rest are this TU's singles.
static std::vector<reasampler::ActionTableRow> buildMainActionTable() {
    using reasampler::ActionTableRow;
    std::vector<ActionTableRow> rows;

    const auto& cap = capture::captureActionTable();
    for (std::size_t i = 0; i < cap.size(); ++i)
        rows.push_back(ActionTableRow{cap[i].commandSuffix, cap[i].descriptionPhrase,
                                      &RunCaptureScopeRow, static_cast<int>(i)});

    // Show/hide the docked bank panel (display-only; never captures/inserts).
    rows.push_back({"TOGGLE_BANK_PANEL", "toggle bank panel", &RunToggleBankPanel});
    rows.push_back({"CAPTURE_ITEM_ASSIGN",
                    "capture selected item into bank + assign to active instance",
                    &RunCaptureItemAssign});
    // Two variants differing ONLY in InsertOptions — native length vs conform opt-in.
    rows.push_back({"INSERT_SELECTED", "insert selected sample at edit cursor",
                    &RunInsertSelected, 0});
    rows.push_back({"INSERT_SELECTED_CONFORM",
                    "insert selected sample at edit cursor (conform to tempo)",
                    &RunInsertSelected, 1});
    // One action fires N captures (per selected item / per razor area); the original
    // selection is restored on every exit path. Bank-only, never places.
    rows.push_back({"CAPTURE_BATCH_ITEMS",
                    "batch capture selected items (one per item)",
                    &RunBatchCaptureItems});
    rows.push_back({"CAPTURE_BATCH_RAZOR", "batch capture razor areas (one per area)",
                    &RunBatchCaptureRazor});
    // Realtime sibling of the offline CAPTURE_TRACK scope, plus its cancel-in-flight
    // companion (stop + restore, non-destructive).
    rows.push_back({"CAPTURE_TRACK_REALTIME", "capture selected track (realtime)",
                    &RunCaptureRealtime});
    rows.push_back({"CANCEL_REALTIME_CAPTURE", "cancel realtime capture",
                    &RunCancelRealtime});
    rows.push_back({"RECAPTURE_FROM_SOURCE", "re-capture from source",
                    &RunRecaptureFromSource});
    // A RENDER_*, not a CAPTURE_*: the id is permanent and is the most durable
    // statement the codebase makes about which pillar a feature belongs to.
    rows.push_back({"RENDER_TRACK_IN_PLACE",
                    "render selected track to a new track (source moves to Design)",
                    &RunRenderTrackInPlace});
    // Invoked by a ReaSampler 9000 instance over the VST3 host bridge (and bindable, so a
    // stranded request can be landed by hand). The suffix is the wire contract itself —
    // core/wire/bake_wire owns the spelling both artifacts read.
    rows.push_back({reasampler::wire::kBakeActionSuffix,
                    "land pending ReaSampler 9000 resample bake",
                    &RunResampleBake});
    rows.push_back({"EXPORT_BANK_PACKAGE", "export active bank as package",
                    &RunExportBankPackage});
    rows.push_back({"IMPORT_BANK_PACKAGE", "import bank package (.rsbank)",
                    &RunImportBankPackage});
    rows.push_back({"SHOW_VERSION", "show version", &RunShowVersion});

    return rows;
}

// The timer callback REAPER runs periodically (registered via "timer"). It only
// forwards to the session poll — cheap per tick (reads the active project id and
// its .rpp path, acts only on a change).
static void OnTimer()
{
    // Advance any in-flight realtime capture FIRST, so a project switch is caught and
    // torn down/restored before session.poll() reacts to that switch. LOAD-BEARING:
    // the idle fast-path is a SINGLE POINTER TEST — drive only when a capture is live.
    if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session);

    g_session.poll();

    // persist stays MODEL-ONLY (loads the saved view model but does not apply
    // visibility, to avoid coupling persist to the view shell); poll() raises a
    // one-shot load signal that we drain here to reapply the SAVED active mode so a
    // project saved in Design mode parks Arrange tracks automatically. The same
    // signal re-arms the bank panel's new-content detector — notified BEFORE the
    // reapply so re-arm and model restore ride the one load event (otherwise
    // pre-existing tracks can be mis-detected as "new" and mass-tagged).
    if (g_session.consumeLoadSignal()) {
        reasampler::bankPanelNotifyProjectLoaded();
        // Reconcile lane ownership against the live project's lanes (P_LANENAME,
        // the cross-session source of truth) BEFORE reapplying visibility. Never
        // re-mints, never mass-tags.
        reasampler::reconcileManagedLanes(g_session.view(), nullptr);
        reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
    }

    reasampler::bankPanelRefresh();  // cheap fingerprint compare; no-op when unchanged/closed
}

// A Ctrl-Z/Ctrl-Shift-Z rolls back/forward the "reasampler" project ext state on disk
// but keeps the SAME project identity, so the timer's identity poll never re-reads
// ext state on undo/redo — the in-memory book/view would stay stale until
// close+reopen. REAPER's projectconfig fires BeginLoadProjectState on every
// project-state (re)load INCLUDING undo/redo (isUndo == true for both); we hook it.
//
// TIMING: BeginLoadProjectState fires BEFORE any state restore, so reading
// GetProjExtState here would return the PRE-undo value. Instead we raise a one-shot
// reload request that OnTimer's poll() drains on the NEXT tick, once REAPER has
// finished restoring the <EXTSTATE> block. A normal project open also fires this
// (isUndo=false); ignored here so a normal open flows solely through the timer's
// identity-transition Load path (no double load).
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
{
    if (isUndo)
        g_session.requestReload();
}

// Intentional no-ops: ReaSampler stores state via project EXT STATE, not this
// extension's own project lines. The struct is registered ONLY for the
// BeginLoadProjectState undo/redo notification.
static bool OnProcessExtensionLine(const char* /*line*/, ProjectStateContext* /*ctx*/,
                                   bool /*isUndo*/, project_config_extension_t* /*reg*/)
{
    return false;  // we own no project lines — ext state carries our data
}

static void OnSaveExtensionConfig(ProjectStateContext* /*ctx*/, bool /*isUndo*/,
                                  project_config_extension_t* /*reg*/)
{
}

// Storage must outlive registration — REAPER holds this pointer until we unregister it.
static project_config_extension_t g_projectConfig{
    &OnProcessExtensionLine,
    &OnSaveExtensionConfig,
    &OnBeginLoadProjectState,
    nullptr,  // userData
};

// REAPER calls this for every action fired in the MAIN section; claim only our own id,
// return false otherwise so REAPER keeps looking. This TU's own family dispatches through
// the registration table; the other families claim their own ids after it.
static bool OnHookCommand(int command, int /*flag*/)
{
    if (command == 0) return false;
    if (reasampler::actionTableHandleCommand(command)) return true;
    if (reasampler::designViewHandleCommand(command)) return true;
    if (reasampler::bankHandleCommand(command)) return true;
    if (reasampler::ingestHandleCommand(command)) return true;
    return false;
}

// "hookcommand" covers the main section only, so actions we published into another
// section arrive here instead. Partitioning contract: root `CLAUDE.md` §"REAPER
// extension contract".
static bool OnHookCommand2(KbdSectionInfo* /*sec*/, int command, int /*val*/, int /*val2*/,
                           int /*relmode*/, HWND /*hwnd*/)
{
    if (command == 0) return false;
    return reasampler::ingestHandleSectionCommand(command);
}

// REAPER polls this to render each of OUR actions' checked state in menus/toolbars.
// Return 1 (on) / 0 (off) for ids we own, -1 for everything else (per the contract).
static int OnToggleAction(int command)
{
    if (command != 0 && command == g_cmdToggleBankPanel)
        return reasampler::bankPanelIsOpen() ? 1 : 0;
    return -1;  // not ours / non-toggling
}

extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
    REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
{
    if (!rec)
    {
        // rec == nullptr => REAPER is UNLOADING us.
        if (g_rec)
        {
            // Abort any in-flight realtime capture FIRST, while the API pointers are
            // still live, so we never leave a temp track, an armed track, or an
            // altered transport/cursor in the user's project on unload.
            capture::AbortRealtimeCaptureForUnload(g_session);

            g_rec->Register("-timer", (void*)&OnTimer);
            g_rec->Register("-projectconfig", (void*)&g_projectConfig);
            g_rec->Register("-toggleaction", (void*)&OnToggleAction);
            g_rec->Register("-hookcommand", (void*)&OnHookCommand);
            g_rec->Register("-hookcommand2", (void*)&OnHookCommand2);
            reasampler::designViewUnregisterActions(g_rec);
            reasampler::bankUnregisterActions(g_rec);
            reasampler::ingestUnregisterActions(g_rec);
            // This TU's own family, reverse table order; each '-command_id'
            // re-presents the SAME interned pointer used at register.
            reasampler::unregisterActionTable(g_rec);
            // Retire the REMOVED command ids (command_id only — we never held a gaccel
            // for them this session).
            for (const char* suffix : kRetiredCaptureCmdSuffixes)
                g_rec->Register("-command_id", (void*)reasampler::channelIdFor(suffix));
        }
        // Before dropping the API pointers: DockWindowRemove/DestroyWindow need them live.
        reasampler::bankPanelShutdown();
        g_rec = nullptr;
        return 0;
    }

    // ABI guard: the struct layout we compiled against must match this REAPER.
    if (rec->caller_version != REAPER_PLUGIN_VERSION)
        return 0;

    // Resolve every REAPER API function pointer. Returns the number that FAILED
    // to load; 0 == success. Non-zero usually means REAPER is older than our SDK.
    if (REAPERAPI_LoadAPI(rec->GetFunc) != 0)
        return 0;

    g_hInst = hInstance;
    g_rec   = rec;

    // Point the bank panel at the live session BEFORE registering its action, so a
    // toggle firing immediately has a session to read. Does not open the window.
    reasampler::bankPanelInit(&g_session);

    {
        const std::vector<reasampler::ActionTableRow> rows = buildMainActionTable();
        reasampler::registerActionTable(rec, rows.data(), rows.size());
    }

    // The panel toggle renders a checked state — resolve its minted id once and
    // register the toggleaction hook that reports it.
    g_cmdToggleBankPanel = reasampler::actionTableCommandId("TOGGLE_BANK_PANEL");
    if (g_cmdToggleBankPanel)
        rec->Register("toggleaction", (void*)&OnToggleAction);

    // Each family mints its own command_id + gaccel, shares g_session, and is routed
    // by the same hookcommand below. Registered before the hook so every id is
    // minted first.
    reasampler::designViewRegisterActions(rec, &g_session);
    reasampler::bankRegisterActions(rec, &g_session);
    reasampler::ingestRegisterActions(rec, &g_session);

    rec->Register("hookcommand", (void*)&OnHookCommand);
    rec->Register("hookcommand2", (void*)&OnHookCommand2);

    // Drives project-load / Save-As detection: the timer polls the active project
    // each tick; on a project load it reloads the bank from ext state, on a Save-As
    // it relocates the bank folder under the new .rpp.
    rec->Register("timer", (void*)&OnTimer);

    // An UNDO/REDO state restore reloads the session's book + view from the restored
    // ext state. The timer's identity poll cannot see an undo (same project
    // identity), so this hook owns it (see OnBeginLoadProjectState).
    rec->Register("projectconfig", (void*)&g_projectConfig);

    return 1;  // success — REAPER keeps us loaded
}
