// realtime_record.cpp — pure logic for the realtime-record backend (M8). See header.
// NO REAPER types; unit-tested by tests/test_realtime_record.cpp.

#include "realtime_record.h"

namespace reasampler {

RecordModePlan recordModePlanFor(int channelCount, OutputTap tap) {
    RecordModePlan p;

    // Stereo vs mono output recording, latency-compensated either way so the
    // recorded file lines up with the source. A request asking for <= 1 channel
    // records mono-out; anything else records stereo-out. (Higher channel counts
    // still record stereo-out here — REAPER's output-record modes are mono/stereo
    // only; a >2-channel realtime capture is out of scope for this increment.)
    p.recMode = (channelCount <= 1) ? kRecModeMonoOutLatComp
                                    : kRecModeStereoOutLatComp;

    switch (tap) {
        case OutputTap::PostFader:      p.recModeFlags = kRecOutPostFader;      break;
        case OutputTap::PreFx:          p.recModeFlags = kRecOutPreFx;          break;
        case OutputTap::PostFxPreFader: p.recModeFlags = kRecOutPostFxPreFader; break;
    }
    return p;
}

OutputTap outputTapForWetDry(double wetDry) {
    // Fully wet (1.0) taps post-fader; any dry-ward value taps pre-FX — the true
    // pre-FX dry that offline render cannot produce (the realtime backend's whole
    // reason to exist for the M10 null test). PostFxPreFader is an explicit future
    // option, not reachable from the wet/dry axis, so it is not returned here.
    return (wetDry >= 1.0) ? OutputTap::PostFader : OutputTap::PreFx;
}

Sample sampleFromRecordedCapture(const RecordedCapture& cap) {
    Sample s;
    // Same id shape as the offline path: "cap-<tag>-<fileName>" would need the file
    // name; here the recorded file name is the tail of relativePath. Keep the id
    // stable + unique via the tag, and include the relative path tail so two
    // captures with the same tag (impossible in practice) still differ.
    s.id           = "cap-" + cap.uniqueTag + "-" + cap.relativePath;
    s.displayName  = cap.displayName;
    s.relativePath = cap.relativePath;  // project-relative (invariant)
    s.sourceMode   = cap.sourceMode;
    s.sourceRange.startSeconds = cap.startSeconds;
    s.sourceRange.endSeconds   = cap.endSeconds;
    // PPQ/beats deferred (musical-placement concern) — identical to the offline path.
    s.wetDry        = cap.wetDry;
    s.trackGuids    = cap.trackGuids;
    s.channelCount  = cap.channelCount;
    s.sampleRate    = cap.sampleRate;   // 0 when project rate was unknown
    s.lengthSeconds = cap.endSeconds - cap.startSeconds;
    s.captureTempo  = cap.captureTempo;
    s.captureTimeSigNum   = cap.captureTimeSigNum;    // L7 F1 meter stamp (0/0 = unstamped)
    s.captureTimeSigDenom = cap.captureTimeSigDenom;
    s.tier          = Tier::Scratch;    // captures land in scratch by default
    // contentHash set by the caller (capture_realtime.cpp) after the file is
    // finalized and on disk — the hash is over the finished file bytes. Left empty
    // here because sampleFromRecordedCapture runs before the file exists (the
    // mapping is pure / DAW-free); the shell patches it in after the move+trim.
    // Phase S seam fields (rootNote / loop) left empty (D-B) — same reasoning as the
    // offline path: a realtime record of wet output is not a single played note, so
    // no root note is derivable; loop points are set by a later explicit action.
    s.createdTimestamp = cap.createdTimestamp;
    return s;
}

RecordPhase advanceRecordPhase(RecordPhase current,
                               const RecordTickInputs& inputs,
                               double rangeStartSeconds,
                               double rangeEndSeconds) {
    switch (current) {
        case RecordPhase::Recording: {
            // Transport stopped while we still expected to be recording -> the user
            // (or REAPER) stopped early. Move to the flush wait and finalize whatever
            // was captured up to the stop.
            if (!inputs.transport.recording) return RecordPhase::Finalizing;

            // Reached the range end (latency-compensated play position). >= (not >)
            // so a cursor landing exactly on the end completes.
            if (inputs.transport.playPosition >= rangeEndSeconds)
                return RecordPhase::Finalizing;

            // Self-defense (review §3): the transport is running but the play cursor
            // is not advancing to the end (stuck / looping). Without this the machine
            // stays in Recording forever, leaking the temp track + armed sink. Force
            // the flush wait once wall-clock exceeds the nominal duration + margin.
            const double ceiling =
                (rangeEndSeconds - rangeStartSeconds) + kRecordMarginSeconds;
            if (inputs.elapsedSeconds > ceiling) return RecordPhase::Finalizing;

            return RecordPhase::Recording;
        }

        case RecordPhase::Finalizing: {
            // The transport is stopped; wait for REAPER to flush/close the recorded
            // take on the audio thread. Finalize (move + Sample) only once the file
            // exists AND is stable (review §2) — moving it early races the flush and
            // yields a truncated / missing capture.
            if (inputs.fileReady) return RecordPhase::Done;

            // Bound the wait: a file that never stabilizes fails cleanly rather than
            // hanging the in-flight state for the session.
            if (inputs.finalizingSeconds > kFinalizeFlushCeilingSeconds)
                return RecordPhase::Failed;

            return RecordPhase::Finalizing;
        }

        // Terminal phases are sticky: once the verdict is in, a later tick (a stray
        // extra call before the shell has finished tearing down) must not flip it.
        case RecordPhase::Done:
        case RecordPhase::Failed:
        default:
            return current;
    }
}

bool isStopRequested(RecordPhase phase) {
    return phase != RecordPhase::Recording;
}

bool isTerminalPhase(RecordPhase phase) {
    return phase == RecordPhase::Done || phase == RecordPhase::Failed;
}

} // namespace reasampler
