// render_in_place.cpp — see render_in_place.h.
//
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the
// one TU that defines the API pointers; here they are extern.

#include "shell/capture/render_in_place.h"

#include <string>
#include <vector>

#include "core/capture/capture_name.h"     // captureTrackName
#include "core/capture/insert_plan.h"      // computeInsertMode / InsertOptions
#include "core/capture/render_settings.h"  // CaptureScope
#include "core/capture/tail_control.h"     // TailSetting
#include "core/capture/track_topology.h"   // siblingPlacement
#include "core/view/view_mode_model.h"     // kArrangeModeId / kDesignModeId
#include "shell/capture/capture.h"
#include "shell/capture/capture_orchestrator.h"  // renderOffline
#include "shell/capture/item_read.h"             // itemGuid
#include "shell/capture/scope_resolve.h"         // ResolveScopeSource / trackName
#include "shell/capture/track_guid.h"            // guidString
#include "shell/panel/panel_input.h"             // bankPanelTailSetting
#include "shell/persist/session.h"
#include "shell/view/view.h"                     // applyMode / mintManagedLanes

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountTrackMediaItems
#define REAPERAPI_WANT_CountTracks
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetCursorPosition
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
#define REAPERAPI_WANT_GetProjectPathEx
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_GetTrackColor
#define REAPERAPI_WANT_GetTrackMediaItem
#define REAPERAPI_WANT_InsertMedia
#define REAPERAPI_WANT_InsertTrackInProject
#define REAPERAPI_WANT_SetEditCurPos
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
#define REAPERAPI_WANT_SetOnlyTrackSelected
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_TrackList_AdjustWindows
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"

namespace reasampler::capture {

namespace {

void refuse(const std::string& why) {
    ShowConsoleMsg(("ReaSampler render in place: " + why + "\n").c_str());
}

// Every track's I_FOLDERDEPTH in track order — the flat delta list the pure
// sibling arithmetic reads.
std::vector<int> folderDepths(ReaProject* proj, int count) {
    std::vector<int> depths;
    depths.reserve(static_cast<std::size_t>(count < 0 ? 0 : count));
    for (int i = 0; i < count; ++i) {
        MediaTrack* tr = GetTrack(proj, i);
        depths.push_back(tr ? static_cast<int>(
                                  GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"))
                            : 0);
    }
    return depths;
}

int indexOfTrack(ReaProject* proj, int count, MediaTrack* wanted) {
    for (int i = 0; i < count; ++i)
        if (GetTrack(proj, i) == wanted) return i;
    return -1;
}

void setTrackName(MediaTrack* tr, const std::string& name) {
    // GetSetMediaTrackInfo_String takes a writable buffer even on the set path.
    std::vector<char> buf(name.begin(), name.end());
    buf.push_back('\0');
    GetSetMediaTrackInfo_String(tr, "P_NAME", buf.data(), true);
}

}  // namespace

void RunRenderTrackInPlace(ReaSamplerSession& session) {
    ResolvedSource src;
    std::string why;
    if (!ResolveScopeSource(CaptureScope::Track, src, why)) { refuse(why); return; }
    if (src.sourceTracks.empty() || !src.sourceTracks.front()) {
        refuse("no source track resolved"); return;
    }

    const TailSetting tail = bankPanelTailSetting();
    const CaptureName name = captureNameFor(src.trackNames, /*ordinal=*/0, "capture");

    CaptureRequest req;
    req.sourceMode   = SourceMode::SelectedTracks;
    req.startSeconds = src.startSeconds;   // exact bounds — no rounding
    req.endSeconds   = src.endSeconds;
    req.wetDry       = 1.0;
    req.tailMode     = tail.mode;
    req.tailMs       = tail.manualMs;
    req.sampleRate   = 0;                  // follow project rate
    req.channelCount = 2;
    req.bitDepth     = WavBitDepth::Float32;
    req.baseName     = name.stemBase;
    req.displayName  = name.label;
    req.destination  = CaptureDestination::ProjectMedia;
    // trackGuids left empty: they exist to stamp provenance onto a Sample this verb
    // discards. A multi-track selection is refused inside renderOffline, keyed on the
    // render source, so there is no check to add here.

    const CaptureResult res = renderOffline(CaptureScope::Track, src.sourceTracks, req);
    if (res.status != CaptureStatus::Ok) { refuse(res.message); return; }

    MediaTrack* source = src.sourceTracks.front();
    ReaProject* proj   = EnumProjects(-1, nullptr, 0);

    const int trackCount = CountTracks(proj);
    const int srcIndex   = indexOfTrack(proj, trackCount, source);
    if (srcIndex < 0) {
        refuse("the source track is no longer in the project; the render landed at " +
               res.absolutePath + " but was not placed.");
        return;
    }

    const SiblingPlacement place =
        siblingPlacement(folderDepths(proj, trackCount), srcIndex);

    // Read ONCE, and only to reapply the mode / decide whether the result landed
    // visible — never to choose a tag. Both tags below are absolute.
    const std::string activeMode = session.view().activeModeId();

    Undo_BeginBlock2(nullptr);

    // flags = 0, never 1: flags&1 adds default envelopes/FX, and a default chain
    // would process a render that already carries the source's FX a second time.
    InsertTrackInProject(proj, place.insertIndex, /*flags=*/0);
    MediaTrack* fresh = GetTrack(proj, place.insertIndex);
    if (!fresh) {
        // InsertTrackInProject already mutated the project by this point, so the
        // "no ext-state write -> discard" idiom does not apply here — a discard would
        // leave the orphaned track un-undoable.
        Undo_EndBlock2(nullptr, "ReaSampler: render in place (failed to create result track)",
                       -1);
        refuse("could not create the result track; the render landed at " +
               res.absolutePath + " but was not placed.");
        return;
    }

    // Both writes or none — one alone lands the new track at the wrong nesting level,
    // which is audible in both directions (see siblingPlacement).
    if (place.precedingIndex >= 0) {
        if (MediaTrack* preceding = GetTrack(proj, place.precedingIndex))
            SetMediaTrackInfo_Value(preceding, "I_FOLDERDEPTH",
                                    static_cast<double>(place.precedingDepth));
    }
    SetMediaTrackInfo_Value(fresh, "I_FOLDERDEPTH",
                            static_cast<double>(place.newDepth));

    // GetTrackColor returns the colour already OR'd with 0x1000000 and 0 for "no
    // colour set", which I_CUSTOMCOLOR reads as unused — so one line clones a colour
    // and the absence of one, with no branch.
    SetMediaTrackInfo_Value(fresh, "I_CUSTOMCOLOR",
                            static_cast<double>(GetTrackColor(source)));
    const std::string freshName = captureTrackName(trackName(source));
    setTrackName(fresh, freshName);

    // After every attribute write, per the SDK header's manual-panel-update caveat.
    TrackList_AdjustWindows(false);

    // Unsnapped and unrounded, deliberately: this placement IS the null test performed
    // automatically, so snapping it to the grid would move the audio off the position
    // it was rendered from. InsertOptions{} defaults give native length and no conform.
    const double cursorPos = GetCursorPosition();
    SetOnlyTrackSelected(fresh);
    SetEditCurPos(src.startSeconds, false, false);
    // InsertMedia's int return isn't SDK-documented; treated conservatively as
    // 0 = failure, matching performArrangeDrop — an empty result track would
    // otherwise be a silent no-op, which is exactly what this verb must not produce.
    const bool placed =
        InsertMedia(res.absolutePath.c_str(), computeInsertMode(InsertOptions{})) != 0;
    SetEditCurPos(cursorPos, false, false);
    // The new track is left selected, alone — in the headline case the source is being
    // parked out of sight in the same gesture, so restoring the selection would leave
    // the user selecting an invisible track.

    // Absolute, not mode-following: the source parks on the bench, the result is an
    // Arrange member whatever mode was active. Explicit records rather than untag(),
    // because the record is what the panel's auto-tag detector defers to.
    MembershipIndex& membership = session.view().membership();
    membership.tag(guidString(source), kDesignModeId);
    membership.tag(guidString(fresh), kArrangeModeId);

    // The track is brand new, so its items are exactly the ones just placed. An
    // untagged item would be handed to the detector, which tags to the active mode.
    const int itemCount = CountTrackMediaItems(fresh);
    for (int i = 0; i < itemCount; ++i) {
        if (MediaItem* it = GetTrackMediaItem(fresh, i)) {
            const std::string ig = itemGuid(it);
            if (!ig.empty()) membership.tag(ig, kArrangeModeId);
        }
    }

    mintManagedLanes(session.view(), nullptr);
    applyMode(session.view(), activeMode, nullptr);  // a reapply, never a switch

    Undo_EndBlock2(nullptr, "ReaSampler: render selected track to a new track", -1);

    // Persist outside the block. The offline render's own save gate already forced a
    // saved project, so the Save-As-guarded persist the Design View actions need
    // cannot have anything to prompt for here.
    session.saveToActiveProject();

    if (!placed) {
        refuse("the render landed at " + res.absolutePath +
               " but REAPER refused to place it — the new track is empty.");
        return;
    }

    // Silent on success — the new track is the feedback. Except when it is not: fired
    // outside Arrange the result track is parked, so a silent success would be
    // indistinguishable from a no-op.
    if (activeMode != kArrangeModeId) {
        ShowConsoleMsg(("ReaSampler render in place: created \"" + freshName +
                        "\" in Arrange (switch to Arrange to see it).\n")
                           .c_str());
    }
}

}  // namespace reasampler::capture
