#pragma once
// ext_state_read — the GetProjExtState grow-loop retry policy (shared by the
// extension's persist/usage-scan/landing shells and the instrument's bridge) AND
// its peer, the read-back verdict that says whether a write took — that one bound
// to the instrument's per-instance keys only, for the reason stated at it below.
//
// GetProjExtState writes into a caller-supplied buffer with no query-the-size
// call, so a large value must be read by growing a buffer until it fits
// strictly inside it. Termination taxonomy (each caller folds differently):
//
//   * Absent   — the API returned <= 0 on some attempt: the key holds no value.
//                (persist -> "" empty bank; usage_scan / bridge -> nullopt)
//   * Complete — the written C string fits STRICTLY inside the buffer (size+1 <
//                cap), so it cannot have been clipped: `value` is the whole value.
//   * Overflow — the value never fit under the 16 MB ceiling: unreadable WHOLE,
//                NOT the same as absent. (persist warns on console; usage_scan
//                folds it to the prune fail-safe abort)
//
// `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap).
// Template, statically dispatched per call site — no virtual calls, no
// std::function (hot-path guardrail); the caller binds project/namespace/key
// in a lambda.

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

namespace reasampler::wire {

struct GrowingExtStateRead {
    enum class Status { Absent, Complete, Overflow };
    Status status = Status::Absent;
    int apiReturn = 0;  // the FINAL attempt's return (<= 0 iff Absent); feeds
                        // decodeGetProjExtState on the bridge path unchanged
    std::string value;  // the whole value; meaningful only when Complete
};

template <class ReadFn>
GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) {
    // Start generous; grow ×4 if REAPER reports the value may have been clipped
    // (the return is the value length; equal-to-capacity-minus-NUL is ambiguous,
    // so only a strict fit terminates). Ceiling 16 MB — give up rather than loop
    // forever on a pathological value.
    GrowingExtStateRead result;
    for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
        std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
        const int rv = read(buf.data(), cap);
        result.apiReturn = rv;
        if (rv <= 0) {
            result.status = GrowingExtStateRead::Status::Absent;
            return result;
        }
        buf[static_cast<std::size_t>(cap) - 1] = '\0';  // guard a read() that ignores NUL-termination within cap
        std::string s(buf.data());
        if (static_cast<int>(s.size()) + 1 < cap) {
            result.status = GrowingExtStateRead::Status::Complete;
            result.value = std::move(s);
            return result;
        }
        // else: possibly truncated -> grow and retry.
    }
    result.status = GrowingExtStateRead::Status::Overflow;
    return result;
}

// Did the write we just made take? `readBack` is what the key holds afterwards, and
// `nullopt` means ABSENT specifically — an Overflow read is NOT an absence and the caller
// must route it to its own "could not check" answer rather than folding it in here, or a
// clear would be claimed off a value nothing could read. This exists because
// SetProjExtState's return is the size of the WHOLE extname's state: `banks`,
// `project_guid` and every rsusage_/rsbake_ key count toward it, so it is non-zero whenever
// ANY key under the extname holds a value — keys this write never touched included. On the
// bake's landing path, where `banks` is necessarily already populated (the landing found
// its source there), it is non-zero unconditionally. That return therefore cannot separate
// a key that took the value from one that did not, and re-reading the key is the only
// per-key observation available under a shared extname. An empty `written` is a CLEAR,
// which lands as an absent-or-empty key rather than as those bytes.
//
// SCOPE — the per-instance keys the two artifacts exchange across the plugin/extension
// boundary. The one that earns it is the bake's `rsbake_<guid>`: the instrument publishes a
// request, REAPER dispatches the action, the extension answers over the same key, and an
// instance finding nothing there genuinely cannot tell "never written" from "written and
// did not stick" by any other means. (The `rsusage_` publish rides along because it shares
// the bridge's one prefix-guarded writer; its verdict is currently unread.)
//
// Do NOT bind it to a write both made and read by one call in one process, and above all
// not to one whose verdict becomes CONTROL FLOW: this is a PROOF VALUE reported to the
// user, never a decision about what to do next. `saveToActiveProject` is the standing
// counter-example — its return IS control flow, over undo points — so do not reintroduce
// a read-back there.
//
// `[verify — DAW]` byte equality assumes REAPER stores and returns an ext-state value
// verbatim. Two known ways that could be false: a value REAPER normalises on the round
// trip, and an embedded NUL (SetProjExtState/GetProjExtState are C-string transports, so a
// payload containing one is truncated on write). Either would report a write REAPER did
// take as unconfirmed — tolerable for a proof value, which is the only thing this may
// answer, and the reason the scope above is a hard limit rather than a preference.
inline bool extStateWriteLanded(const std::string& written,
                                const std::optional<std::string>& readBack) {
    if (written.empty()) return !readBack || readBack->empty();
    return readBack && *readBack == written;
}

}  // namespace reasampler::wire
